- config.yaml에서 직접 KIS 설정 관리 (BaseSettings 제거)
- 대시보드 KIS 계좌 잔고 실시간 연동, 로컬 DB 폴백
- 종목 조회 페이지: 시세/호가/일봉차트 (canvas) 구현
- 호가 REST API 엔드포인트 (/api/stocks/{code}/orderbook) 추가
- 토큰 캐시(.auth_cache.json) 저장/복원, 1분 재시도 제한
- KIS WebSocket 자동 연결 + 재연결 로직
- httpx 타임아웃 10초→30초, 라우터 타임아웃 핸들링 (504)
- rate limit: requests_per_second 2.0 (모의투자 초당 2건)
- 사이드바 3메뉴: 실거래 / 과거기록 / 종목조회
98 lines
3.1 KiB
Python
98 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.database import get_db
|
|
from app.models.stock import Stock
|
|
from app.services.market_data import market_data_service
|
|
|
|
router = APIRouter(prefix="/api/stocks", tags=["stocks"])
|
|
|
|
|
|
class StockCreate(BaseModel):
|
|
code: str
|
|
name: str
|
|
market: str = "KRX"
|
|
|
|
|
|
class StockUpdate(BaseModel):
|
|
name: str | None = None
|
|
is_active: bool | None = None
|
|
|
|
|
|
@router.get("/")
|
|
def list_stocks(db: Session = Depends(get_db)) -> list[dict]:
|
|
stocks = db.query(Stock).all()
|
|
return [
|
|
{
|
|
"code": s.code,
|
|
"name": s.name,
|
|
"market": s.market,
|
|
"is_active": s.is_active,
|
|
"created_at": s.created_at.isoformat() if s.created_at else None,
|
|
}
|
|
for s in stocks
|
|
]
|
|
|
|
|
|
@router.post("/")
|
|
def add_stock(data: StockCreate, db: Session = Depends(get_db)) -> dict:
|
|
existing = db.query(Stock).filter(Stock.code == data.code).first()
|
|
if existing:
|
|
raise HTTPException(status_code=409, detail="이미 존재하는 종목입니다")
|
|
|
|
stock = Stock(code=data.code, name=data.name, market=data.market)
|
|
db.add(stock)
|
|
db.commit()
|
|
db.refresh(stock)
|
|
return {"code": stock.code, "name": stock.name, "market": stock.market, "is_active": stock.is_active}
|
|
|
|
|
|
@router.delete("/{stock_code}")
|
|
def remove_stock(stock_code: str, db: Session = Depends(get_db)) -> dict:
|
|
stock = db.query(Stock).filter(Stock.code == stock_code).first()
|
|
if not stock:
|
|
raise HTTPException(status_code=404, detail="종목을 찾을 수 없습니다")
|
|
|
|
stock.is_active = False
|
|
db.commit()
|
|
return {"message": "종목이 비활성화되었습니다", "code": stock_code}
|
|
|
|
|
|
@router.get("/{stock_code}/price")
|
|
async def get_price(stock_code: str) -> dict:
|
|
try:
|
|
price = await market_data_service.get_current_price(stock_code)
|
|
except (httpx.TimeoutException, httpx.ConnectError):
|
|
raise HTTPException(status_code=504, detail="KIS 서버 응답 시간 초과")
|
|
if not price:
|
|
raise HTTPException(status_code=404, detail="시세 조회 실패")
|
|
return price
|
|
|
|
|
|
@router.get("/{stock_code}/chart")
|
|
async def get_chart(stock_code: str, days: int = 30) -> list[dict]:
|
|
from datetime import datetime, timedelta
|
|
|
|
end = datetime.now().strftime("%Y%m%d")
|
|
start = (datetime.now() - timedelta(days=days)).strftime("%Y%m%d")
|
|
try:
|
|
chart = await market_data_service.get_daily_chart(stock_code, start, end, days)
|
|
except (httpx.TimeoutException, httpx.ConnectError):
|
|
raise HTTPException(status_code=504, detail="KIS 서버 응답 시간 초과")
|
|
return chart
|
|
|
|
|
|
@router.get("/{stock_code}/orderbook")
|
|
async def get_orderbook(stock_code: str) -> dict:
|
|
try:
|
|
orderbook = await market_data_service.get_orderbook(stock_code)
|
|
except (httpx.TimeoutException, httpx.ConnectError):
|
|
raise HTTPException(status_code=504, detail="KIS 서버 응답 시간 초과")
|
|
if not orderbook:
|
|
raise HTTPException(status_code=404, detail="호가 조회 실패")
|
|
return orderbook
|