- 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메뉴: 실거래 / 과거기록 / 종목조회
105 lines
3.6 KiB
Python
105 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.auth import token_manager
|
|
from app.core.database import get_db
|
|
from app.models.stock import Holding, Strategy, Trade, Stock
|
|
from app.engine.scheduler import scheduler
|
|
from app.services.account import account_service
|
|
from app.services.realtime import realtime_service
|
|
|
|
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
|
|
|
|
|
@router.get("/")
|
|
async def dashboard(db: Session = Depends(get_db)) -> dict:
|
|
recent_trades = db.query(Trade).order_by(Trade.created_at.desc()).limit(20).all()
|
|
active_strategies = db.query(Strategy).filter(Strategy.is_active == True).all()
|
|
active_stocks = db.query(Stock).filter(Stock.is_active == True).all()
|
|
|
|
holdings = []
|
|
total_profit = 0.0
|
|
total_invested = 0.0
|
|
total_evaluated = 0.0
|
|
|
|
if token_manager.is_authenticated:
|
|
try:
|
|
balance = await account_service.get_balance()
|
|
for s in balance.get("stocks", []):
|
|
holdings.append({
|
|
"stock_code": s["stock_code"],
|
|
"stock_name": s["stock_name"],
|
|
"qty": s["qty"],
|
|
"avg_price": s["avg_price"],
|
|
"current_price": s["current_price"],
|
|
"profit": s["profit"],
|
|
"profit_rate": s["profit_rate"],
|
|
})
|
|
total_invested += s.get("buy_amount", s["avg_price"] * s["qty"])
|
|
total_evaluated += s.get("eval_amount", s["current_price"] * s["qty"])
|
|
total_profit += s["profit"]
|
|
except Exception:
|
|
pass
|
|
|
|
if not holdings:
|
|
local = db.query(Holding).all()
|
|
for h in local:
|
|
holdings.append({
|
|
"stock_code": h.stock_code,
|
|
"stock_name": h.stock_name,
|
|
"qty": h.qty,
|
|
"avg_price": h.avg_price,
|
|
"current_price": h.current_price,
|
|
"profit": h.profit,
|
|
"profit_rate": h.profit_rate,
|
|
})
|
|
total_invested += h.avg_price * h.qty
|
|
total_evaluated += h.current_price * h.qty
|
|
total_profit += h.profit
|
|
|
|
jobs = []
|
|
if scheduler.running:
|
|
for job in scheduler.get_jobs():
|
|
jobs.append({
|
|
"id": job.id,
|
|
"name": job.name,
|
|
"next_run": str(job.next_run_time) if job.next_run_time else None,
|
|
})
|
|
|
|
return {
|
|
"authenticated": token_manager.is_authenticated,
|
|
"summary": {
|
|
"total_holdings": len(holdings),
|
|
"total_profit": total_profit,
|
|
"total_invested": total_invested,
|
|
"total_evaluated": total_evaluated,
|
|
"profit_rate": (total_profit / total_invested * 100) if total_invested > 0 else 0,
|
|
"active_strategies": len(active_strategies),
|
|
"active_stocks": len(active_stocks),
|
|
},
|
|
"holdings": holdings,
|
|
"recent_trades": [
|
|
{
|
|
"id": t.id,
|
|
"stock_code": t.stock_code,
|
|
"side": t.side,
|
|
"qty": t.qty,
|
|
"price": t.price,
|
|
"status": t.status,
|
|
"created_at": t.created_at.isoformat() if t.created_at else None,
|
|
}
|
|
for t in recent_trades
|
|
],
|
|
"scheduler_jobs": jobs,
|
|
}
|
|
|
|
|
|
@router.get("/status")
|
|
def get_status() -> dict:
|
|
return {
|
|
"authenticated": token_manager.is_authenticated,
|
|
"realtime_connected": realtime_service.is_connected,
|
|
}
|