랭그래프 활용한 주식 정보 도출 프로젝트

- 투자 의견 조회
ㄴ 전반적인 주식 판단 기준(ex. 채무, 리스크 등등)을 기준으로 판단하여 도출
- 투자 추천
ex)
{
  "tickers": [
    "NVDA", "GOOGL", "AAPL"
  ],
  "risk_type": "aggressive"
}
This commit is contained in:
2026-06-19 18:03:05 +09:00
parent 3be7886bfe
commit 315105cebb
19 changed files with 614 additions and 68 deletions
+1 -4
View File
@@ -2,13 +2,10 @@
<module type="PYTHON_MODULE" version="4"> <module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" /> <excludeFolder url="file://$MODULE_DIR$/.venv" />
</content> </content>
<orderEntry type="jdk" jdkName="~/Source/project/STOCK_APP/.venv" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="~/Source/project/STOCK_APP/.venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="PackageRequirementsSettings" />
<component name="PyDocumentationSettings" />
<component name="ReSTService" />
<component name="TestRunnerService" />
</module> </module>
@@ -8,5 +8,6 @@ class Settings(BaseSettings):
watsonx_project_id: str = Field(alias="WATSONX_PROJECT_ID") watsonx_project_id: str = Field(alias="WATSONX_PROJECT_ID")
watsonx_url: str = Field(alias="WATSONX_URL") watsonx_url: str = Field(alias="WATSONX_URL")
hf_token: str = Field(alias="HF_TOKEN") hf_token: str = Field(alias="HF_TOKEN")
serper_api_key: str = Field(alias="SERPER_API_KEY")
settings = Settings() settings = Settings()
+60 -2
View File
@@ -1,6 +1,13 @@
from backend.services.news_service import get_news from backend.services.news_service import get_news
from backend.services.financial_service import get_financial_info from backend.services.financial_service import get_financial_info
from backend.services.technical_service import get_technical_info from backend.services.technical_service import get_technical_info
from backend.services.competitor_service import get_competitor_info
from langchain_core.prompts import ChatPromptTemplate
from backend.ai.llm import hugging_llm
from backend.prompts.all_prompt import REPORT_PROMPT
prompt = ChatPromptTemplate.from_template(REPORT_PROMPT)
report_chain = prompt | hugging_llm
async def news_node(state): async def news_node(state):
news_list = await get_news(state["company_name"]) news_list = await get_news(state["company_name"])
@@ -15,7 +22,58 @@ async def technical_node(state):
return {"technicals": technicals} return {"technicals": technicals}
async def competitor_node(state): async def competitor_node(state):
pass competitors = await get_competitor_info(state["ticker"])
return {"competitors": competitors}
async def report_node(state): async def report_node(state):
pass """
수집된 정보를 LLM 에게 넘기고 분석 요청
1. 뉴스기사
2. 재무정보(시가총액, 매출, 순이익...)
3. 기술적 지표(현재주가, 20일선, 60일선, rsi...)
4. 경쟁회사 재무 정보
"""
news_text = "\n".join([f"제목 : {n.title}\n내용 : {n.snippet}" for n in state["news"]])
financials = state['financials']
financial_text = f"""
시가총액 : {financials['market_cap']}
매출 : {financials['revenue']}
총자산 : {financials['total_assets']}
순이익 : {financials['net_income']}
PER : {financials['pe_ratio']}
PBR : {financials['pb_ratio']}
"""
technicals = state['technicals']
technical_text = f"""
현재주가 : {technicals['current_price']}
20일선 : {technicals['sma20']}
60일선 : {technicals['sma60']}
RSI : {technicals['rsi']}
MACD : {technicals['macd']}
MACD_SIGNAL : {technicals['macd_signal']}
추세 : {technicals['trend']}
"""
competitor_text = "\n".join([f"""
티커 : {c['ticker']}
회사 : {c['company_name']}
시가총액 : {c['market_cap']}
PER : {c['pe_ratio']}
매출성장률 : {c['revenue_growth']}
순이익률 : {c['profit_margin']}
""" for c in state['competitors']])
report = await report_chain.ainvoke(
{
"company_name" : state["company_name"],
"news": news_text,
"financials": financial_text,
"technicals": technical_text,
"competitors": competitor_text,
}
)
return {"report": report.content}
+2 -2
View File
@@ -1,10 +1,10 @@
from typing import TypedDict from typing import TypedDict
class StockAnalysis(TypedDict): class StockAnalysisState(TypedDict):
query:str query:str
ticker:str ticker:str
company_name:str company_name:str
new:list news:list
financials:dict financials:dict
technicals:dict technicals:dict
competitors:list competitors:list
@@ -0,0 +1,30 @@
from langgraph.graph import StateGraph, START, END
from backend.graph.state import StockAnalysisState
from backend.graph.nodes import news_node, financial_node, competitor_node, report_node, technical_node
import logging
logger = logging.getLogger(__name__)
def build_stock_graph():
# 그래프 생성
graph = StateGraph(StockAnalysisState)
# 노드 등록
graph.add_node("news", news_node)
graph.add_node("financial", financial_node)
graph.add_node("technical", technical_node)
graph.add_node("competitors", competitor_node)
graph.add_node("report", report_node)
# 연결
graph.add_edge(START, "news")
graph.add_edge("news", "financial")
graph.add_edge("financial", "technical")
graph.add_edge("technical", "competitors")
graph.add_edge("competitors", "report")
graph.add_edge("report", END)
# 실행
compiled = graph.compile()
logger.info("LangGraph 주식 분석 그래프 빌드 완료")
return compiled
+14 -20
View File
@@ -4,40 +4,34 @@ from contextlib import asynccontextmanager
from backend.routers.stock_router import router as stock_router from backend.routers.stock_router import router as stock_router
# from backend.repository.db_init import Base, SessionLocal, engine from backend.repository.db_init import Base, SessionLocal, engine
#
# from backend.routers.evalu_router import router as evalu_router # from backend.routers.evalu_router import router as evalu_router
# from backend.routers.assistant_router import router as assistant_router # from backend.routers.assistant_router import router as assistant_router
# from backend.routers.call_router import router as call_router # from backend.routers.call_router import router as call_router
# from backend.repository.seed import seed_customers # from backend.repository.seed import seed_customers
# @asynccontextmanager @asynccontextmanager
# async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
# print("서버 시작") print("서버 시작")
#
# # Base 에 등록된 모든 모델에 테이블 자동 생성 # Base 에 등록된 모든 모델에 테이블 자동 생성
# Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
# print("[DB] 테이블 생성 완료 (또는 이미 존재)") print("[DB] 테이블 생성 완료 (또는 이미 존재)")
#
# db = SessionLocal() yield
# try: print("서버 종료")
# # 기본 user 데이터 삽입
# seed_customers(db)
# finally:
# db.close()
#
# yield
# print("서버 종료")
# app = FastAPI() # app = FastAPI()
# app = FastAPI(title="상담 LLM", version="1.0", lifespan=lifespan) # app = FastAPI(title="상담 LLM", version="1.0", lifespan=lifespan)
app = FastAPI(title="$ Stock AI", version="1.0", description=""" app = FastAPI(title="$ Stock AI", version="1.0", lifespan=lifespan, description="""
#### $주식 AI 분석 FastAPI + LangGraph + yfinance + ChromaDB #### $주식 AI 분석 FastAPI + LangGraph + yfinance + ChromaDB
### 주요 기능 ### 주요 기능
| 번호 | 기능 | 엔드포인트 | | 번호 | 기능 | 엔드포인트 |
| --- | --- | --- | | --- | --- | --- |
|1 | 기업 종합 분석 | 'POST /api/stock/analysis' |1 | 기업 종합 분석 | 'POST /api/stock/analysis'
|2 | 투자 의견 조회 | 'GET /api/stock/opinion/{analysis_id}'
""", """,
) )
@@ -0,0 +1,43 @@
REPORT_PROMPT = """
당신은 월가 애널리스트입니다.
회사명 :
{company_name}
최근 뉴스 :
{news}
재무정보 :
{financials}
기술적 지표 :
{technicals}
경쟁사 :
{competitors}
다음 형식으로 분석하세요.
1. 기업 개요
2. 최근 뉴스 핵심 요약
3. 재무 상태 분석
4. 기술적 분석
5. 경쟁사 비교
6. 주요 리스크
7. 종합의견
반드시 구체적인 수치를 인용하세요.
"""
INVESTOR_SENTIMENT_PROMPT = """
다음 뉴스들의 전체적인 투자 심리를 분석하시오.
{news_text}
반드시 JSON 형식으로 응답하세요.
{{
"positive":0,
"negative":0,
"neutral":0,
}}
"""
@@ -0,0 +1,23 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from pathlib import Path
Path("db").mkdir(parents=True, exist_ok=True)
# echo sql 구문 출력시키는
engine = create_engine('sqlite:///db/stock.db', echo=True)
# 세션 팩토리 생성
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
# Base = declarative_Base()
class Base(DeclarativeBase):
pass
# session 을 다른 모듈에서 사용할 수 있도록 제공
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@@ -0,0 +1,28 @@
from backend.repository.db_init import Base
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
from sqlalchemy import JSON, ForeignKey, String
class StockAnalysis(Base):
__tablename__ = "stock_analysis"
analysis_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
ticker:Mapped[str]
company_name:Mapped[str]
analysis_json: Mapped[dict] = mapped_column(JSON)
report:Mapped[str]
created_at:Mapped[datetime] = mapped_column(default=datetime.now, nullable=False)
opinion = relationship("InvestmentOpinion", back_populates="analysis", uselist=False)
class InvestmentOpinion(Base):
__tablename__ = "investment_opinion"
opinion_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
analysis_id: Mapped[int] = mapped_column(ForeignKey("stock_analysis.analysis_id"), unique=True)
opinion: Mapped[str]
# 투자의견 : Buy, sell.....
rating:Mapped[str] = mapped_column(String(20), nullable=True)
score:Mapped[int] = mapped_column(default=0)
created_at:Mapped[datetime] = mapped_column(default=datetime.now, nullable=False)
analysis = relationship("StockAnalysis", back_populates="opinion")
@@ -1,12 +1,30 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from backend.schemas.stock_schemas import StockAnalyzeReq from backend.schemas.stock_schemas import StockAnalyzeReq
from backend.services.stock_service import StockService from backend.services.stock_service import StockService
from sqlalchemy.orm import Session
from backend.repository.db_init import get_db
from backend.services.stock_service import get_stock_service
from backend.schemas.stock_schemas import RecommendStock
from backend.services.recommend_service import generate_portfolio_report, portfolio_service
router = APIRouter(prefix="/api/stock", tags=["기헙종합분석"]) router = APIRouter(prefix="/api/stock", tags=["기헙종합분석"])
@router.post("/analyze", summary="기업 종합 분석", description="자연어로 해당기업을 요청하면 뉴스, 재무, 기술적, 경쟁사 분석을 수행합니다.", ) @router.post("/analyze", summary="기업 종합 분석", description="자연어로 해당기업을 요청하면 뉴스, 재무, 기술적, 경쟁사 분석을 수행합니다.", )
async def stock_analyze(req:StockAnalyzeReq): async def stock_analyze(req:StockAnalyzeReq, db:Session=Depends(get_db)):
service = StockService() service = get_stock_service()
return await service._extract(req.query) return await service.analyze(req.query, db)
@router.get("/opinion/{analysis_id}", summary="투자 의견 조회", description="", )
async def stock_analyze_opinion(analysis_id:int, db:Session=Depends(get_db)):
service = get_stock_service()
return await service.opinion_service(analysis_id, db)
@router.post("/recommend", summary="투자 추천", description="", )
async def stock_recommend(req: RecommendStock, db: Session=Depends(get_db)):
result = portfolio_service(req, db)
report = await generate_portfolio_report(result['portfolio'], req.risk_type)
return {
"risk_type":req.risk_type,
"portfolio":result['portfolio'],
"report":report,
}
@@ -1,5 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
from enum import Enum
class StockAnalyzeReq(BaseModel): class StockAnalyzeReq(BaseModel):
query: str query: str
@@ -7,3 +7,15 @@ class StockAnalyzeReq(BaseModel):
class TickerInfo(BaseModel): class TickerInfo(BaseModel):
ticker: str ticker: str
company_name: str company_name: str
# enum
# risk_type : aggressive, balanced, conservative
class RiskType(str, Enum):
AGGRESSIVE = "aggressive"
BALANCED = "balanced"
CONSERVATIVE = "conservative"
class RecommendStock(BaseModel):
tickers: list[str]
risk_type : RiskType
@@ -0,0 +1,41 @@
import yfinance as yf
COMPETITOR_MAP = {
"NVDA": ["AMD", "INTC", "TSM", "AVGO"],
"AMD": ["NVDA", "INTC", "TSM"],
"TSLA": ["RIVN", "GM", "F"],
"AAPL": ["MSFT", "GOOGL", "AMZN"],
"MSFT": ["AAPL", "GOOGL", "AMZN"],
"GOOGL": ["MSFT", "META", "AMZN"],
}
async def get_competitor_info(ticker: str):
"""
경쟁 관계 회사에 대한 핵심 정보 추출
"""
competitors_tickers = COMPETITOR_MAP.get(ticker, [])
results =[]
for comp in competitors_tickers:
company = yf.Ticker(comp)
info = company.info
results.append(
{
#
"ticker": comp,
# 회사 이름
"company_name": info.get("longName"),
# 시가총액
"market_cap": info.get("marketCap"),
# per
"pe_ratio": info.get("pe_ratio"),
# 매출 성장률
"revenue_growth": info.get("revenue_growth"),
# 순 이익률
"profit_margin": info.get("profitMargins"),
}
)
return results
@@ -1,4 +1,5 @@
import yahooquery as yf import yfinance as yf
# from yahooquery import Ticker
async def get_financial_info(ticker: str): async def get_financial_info(ticker: str):
""" """
@@ -7,7 +8,7 @@ async def get_financial_info(ticker: str):
company = yf.Ticker(ticker) company = yf.Ticker(ticker)
info = company.info info = company.info
income = company.income_stmtf income = company.income_stmt
balance = company.balance_sheet balance = company.balance_sheet
cashflow = company.cash_flow cashflow = company.cash_flow
@@ -31,6 +32,5 @@ async def get_financial_info(ticker: str):
# per # per
"pe_ratio": info.get("trailingPE"), "pe_ratio": info.get("trailingPE"),
# pbr # pbr
"pb_ratio":info.get("priceToBook") "pb_ratio": info.get("priceToBook"),
} }
@@ -1,6 +1,6 @@
from langchain_community.utilities import GoogleSerperAPIWrapper from langchain_community.utilities import GoogleSerperAPIWrapper
from backend.schemas.news_schemas import NewsItem from backend.schemas.news_schemas import NewsItem
from backend.config.settings import settings
async def get_news(company_name: str): async def get_news(company_name: str):
@@ -8,8 +8,9 @@ async def get_news(company_name: str):
구글 뉴스 검색 후 구글 뉴스 검색 후
title, snippet, url, source, date 추출 title, snippet, url, source, date 추출
""" """
search = GoogleSerperAPIWrapper(type="news") search = GoogleSerperAPIWrapper(type="news", serper_api_key=f"{settings.serper_api_key}")
results = await search.results(f"{company_name} stock news") # results = search.aresults(f"{company_name} stock news")
results = await search.aresults(f"{company_name} stock news")
news_list = [] news_list = []
for item in results['news'][:10]: for item in results['news'][:10]:
@@ -0,0 +1,71 @@
from sqlalchemy.orm import Session
from backend.schemas.stock_schemas import RecommendStock, RiskType
from backend.repository.models import StockAnalysis
from fastapi import HTTPException
from backend.ai.llm import hugging_llm
def portfolio_service(req, db):
"""
사용자가 입력한 종목에 대해서 분석정보 찾기
risk_type: 비중 조절
"""
analyses = db.query(StockAnalysis).filter(StockAnalysis.ticker.in_(req.tickers)).all()
portfolio_items = []
for analysis in analyses:
if not analysis.opinion:
continue
portfolio_items.append(
{
"ticker": analysis.ticker,
"score": analysis.opinion.score,
"rating": analysis.opinion.rating,
}
)
if not portfolio_items:
raise HTTPException(status_code=5000, detail="분석된 종목을 찾을 수 없습니다.")
# 투자성향
risk_type = req.risk_type
import math
for item in portfolio_items:
score = item["score"]
if risk_type == RiskType.AGGRESSIVE:
item['adjusted_score'] = score ** 1.5
elif risk_type == RiskType.CONSERVATIVE:
item['adjusted_score'] = math.sqrt(score)
else:
item['adjusted_score'] = score
total_adjusted = sum(item['adjusted_score'] for item in portfolio_items)
for item in portfolio_items:
item['weight'] = round(item['adjusted_score'] / total_adjusted * 100,2)
return {"risk_type": req.risk_type, "portfolio": portfolio_items}
async def generate_portfolio_report(portfolio_items, risk_type):
portfolio_text = "\n".join([f"{item['ticker']}: {item['weight']}%" for item in portfolio_items])
prompt = f"""
당신은 월가의 포트폴리오 전략가입니다.
투자성향
{risk_type.value}
포트폴리오:
{portfolio_text}
다음을 작성하시오.
1. 포트폴리오 요약
2. 강점
3. 약점
4. 주요 리스크
5. 추천 투자기간
6. 리밸런싱 전략
"""
result = await hugging_llm.ainvoke(prompt)
return result.content
@@ -0,0 +1,137 @@
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from backend.ai.llm import hugging_llm
from backend.prompts.all_prompt import INVESTOR_SENTIMENT_PROMPT
def get_rating(score):
# 최종판단
if score >= 85:
return "Strong Buy"
elif score >= 70:
return "Buy"
elif score >= 50:
return "Hold"
elif score >= 30:
return "Sell"
return "Strong Sell"
# 재무 정보 1. PER(20점)
def get_per_score(per):
if per is None:
return 0
if per < 20:
return 20
elif per < 35:
return 15
return 10
# 2. 순이익률
def get_profit_margin(financials):
net_income = financials.get("net_income")
revenue = financials.get("revenue")
if not revenue:
return 0, 0
profit_margin = net_income / revenue
profit_score = 0
if profit_margin > 0.3:
profit_score = 20
elif profit_margin > 0.15:
profit_score = 15
else:
profit_score = 10
return profit_score, profit_margin
# 3. 기술분석 (20점)
def get_tech_score(technicals):
technicals_score = 0
trend = technicals['trend']
if trend == 'bullish':
technicals_score += 10
macd = technicals['macd']
macd_signal = technicals['macd_signal']
if macd > macd_signal:
technicals_score += 10
return technicals_score
# 4. 경쟁사 분석(20점)
def get_com_score(competitor, per, profit_margin):
# 경쟁사 per 평균 비교
competitors_score = 0
pe_list = [c["pe_ratio"] for c in competitor if c["pe_ratio"] is not None]
if not pe_list:
avg_pe = None
else:
avg_pe = sum(pe_list) / len(pe_list)
if avg_pe is not None and per < avg_pe:
competitors_score += 10
profit_margin_list = [c["profit_margin"] for c in competitor if c["profit_margin"] is not None]
avg_profit_margin = sum(profit_margin_list) / len(profit_margin_list)
if profit_margin > avg_profit_margin:
competitors_score += 10
return competitors_score
# 5. 뉴스 분석(20점)
async def get_news_score(news_list):
# 뉴스 투자 심리
news_text = "\n".join(
[f"제목 : {n['title']}\n내용 : {n['snippet']}" for n in news_list]
)
prompt = ChatPromptTemplate.from_template(INVESTOR_SENTIMENT_PROMPT)
chain = prompt | hugging_llm | JsonOutputParser()
sentiment = await chain.ainvoke({"news_text":news_text})
total = sentiment["positive"] + sentiment["negative"] + sentiment["neutral"]
if total == 0:
return 0
positive_ratio = sentiment["positive"] / total
return round(positive_ratio * 20)
async def evaluation(analysis):
score = 0
data = analysis.analysis_json
# 1. per 점수
pe_ratio = data['financials'].get('pe_ratio')
per_score = get_per_score(pe_ratio)
score += per_score
# 2. 순이익률 점수
profit_score, profit_margin = get_profit_margin(data['financials'])
score += profit_score
# 3. 기술적 점수
technicals_score = get_tech_score(data["technicals"])
score += technicals_score
# 4. 경쟁사 점수
competitor_score = get_com_score(data['competitors'], pe_ratio, profit_margin)
score += competitor_score
# 5. 뉴스 점수
news_score = await get_news_score(data['news'])
score += news_score
return {
"total_score": score,
"per_score": per_score,
"profit_score": profit_score,
"technical_score": technicals_score,
"competitor_score": competitor_score,
"news_score": news_score
}
@@ -1,6 +1,12 @@
from backend.schemas.stock_schemas import TickerInfo from backend.schemas.stock_schemas import TickerInfo
import yahooquery as yq import yahooquery as yq
from fastapi import HTTPException 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 = { COMPANY_MAP = {
"NVDA": { "NVDA": {
@@ -115,6 +121,8 @@ class StockService:
if alias.lower() in query: if alias.lower() in query:
return TickerInfo(ticker = ticker, company_name = info["company_name"]) return TickerInfo(ticker = ticker, company_name = info["company_name"])
return None
def _extract_company_keyword(self, query:str): def _extract_company_keyword(self, query:str):
""" """
query : 엔비디아 분석해줘 query : 엔비디아 분석해줘
@@ -146,3 +154,88 @@ class StockService:
except Exception: except Exception:
return None 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
@@ -1,4 +1,4 @@
import yahooquery as yf import yfinance as yf
from ta.trend import MACD, SMAIndicator from ta.trend import MACD, SMAIndicator
from ta.momentum import RSIIndicator from ta.momentum import RSIIndicator
@@ -10,7 +10,7 @@ async def get_technical_info(ticker: str):
# 주가 데이터 다운로드 # 주가 데이터 다운로드
df = yf.download(ticker, period="1y", interval="1d") df = yf.download(ticker, period="1y", interval="1d")
# 종가 가져오기 # 종가 가져오기
close = df['Close'].squeeze() close = df["Close"].squeeze()
macd = MACD(close) macd = MACD(close)
macd_value = float(macd.macd().iloc[-1]) macd_value = float(macd.macd().iloc[-1])
@@ -25,12 +25,11 @@ async def get_technical_info(ticker: str):
# 60일선 # 60일선
"sma60": float(SMAIndicator(close, window=60).sma_indicator().iloc[-1]), "sma60": float(SMAIndicator(close, window=60).sma_indicator().iloc[-1]),
# rsi # rsi
"rsi":float(SMAIndicator(close, window=14).rsi().iloc[-1]), "rsi": float(RSIIndicator(close, window=14).rsi().iloc[-1]),
# macd # macd
"macd":macd.macd(), "macd": macd_value,
# macd_signal # macd_signal
"macd_signal": signal_value, "macd_signal": signal_value,
# trend # trend
"trend": trend, "trend": trend,
} }
Binary file not shown.