""" 매매일지 API 엔드포인트 - GET /: 최근 AI 생성 매매일지 목록 - GET /stats: 일지 통계 (전략별, 감정 분포) - GET /{journal_id}: 특정 일지 상세 - POST /generate/{trade_id}: 특정 거래에 대한 일지 즉시 생성 """ from fastapi import APIRouter, Depends, HTTPException, Query from app.core.security import require_api_key from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc, func from typing import Optional from datetime import datetime, timezone import logging def _utc_iso(dt: datetime | None) -> str | None: """UTC naive datetime → ISO 8601 with +00:00 (JS가 UTC로 올바르게 파싱)""" if dt is None: return None return dt.replace(tzinfo=timezone.utc).isoformat() from app.core.database import get_db from app.models.models import TradeJournal, TradeHistory logger = logging.getLogger(__name__) router = APIRouter() @router.get("") async def list_journals( limit: int = Query(default=20, ge=1, le=100), offset: int = Query(default=0, ge=0), stock_code: Optional[str] = None, db: AsyncSession = Depends(get_db), ): """최근 AI 생성 매매일지 목록 조회""" try: query = select(TradeJournal).order_by(desc(TradeJournal.journal_date)) if stock_code: query = query.where(TradeJournal.stock_code == stock_code) query = query.offset(offset).limit(limit) result = await db.execute(query) journals = result.scalars().all() return [ { "id": j.id, "trade_id": j.trade_id, "stock_code": j.stock_code, "stock_name": j.stock_name, "journal_date": _utc_iso(j.journal_date) if j.journal_date else None, "content": j.content, "strategy_name": j.strategy_name, "mood": j.mood, "tags": j.tags, "created_at": _utc_iso(j.created_at), } for j in journals ] except Exception as e: logger.error(f"journals list error: {e}", exc_info=True) return [] @router.get("/stats") async def get_journal_stats(db: AsyncSession = Depends(get_db)): """일지 통계: 전략별 / 감정별 분포.""" try: result_mood = await db.execute( select(TradeJournal.mood, func.count(TradeJournal.id).label("cnt")) .group_by(TradeJournal.mood) ) mood_dist = {row[0] or "unknown": row[1] for row in result_mood.fetchall()} result_strat = await db.execute( select(TradeJournal.strategy_name, func.count(TradeJournal.id).label("cnt")) .group_by(TradeJournal.strategy_name) ) strat_dist = {row[0] or "unknown": row[1] for row in result_strat.fetchall()} total = sum(mood_dist.values()) return { "total": total, "by_mood": mood_dist, "by_strategy": strat_dist, } except Exception as e: logger.error(f"journal stats error: {e}", exc_info=True) return {"total": 0, "by_mood": {}, "by_strategy": {}} @router.post("/generate/{trade_id}", dependencies=[Depends(require_api_key)]) async def generate_journal_for_trade_id( trade_id: int, db: AsyncSession = Depends(get_db), ): """특정 완료 거래(trade_id)에 대해 템플릿 기반 매매일지를 즉시 생성.""" from app.services.trade_analyzer import ( analyze_performance, generate_journal_for_trade, create_journal_record, ) # 기존 일지 확인 existing = await db.execute( select(TradeJournal).where(TradeJournal.trade_id == trade_id) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=409, detail="이미 일지가 존재합니다.") # 거래 조회 trade_result = await db.execute( select(TradeHistory).where(TradeHistory.id == trade_id) ) trade = trade_result.scalar_one_or_none() if not trade: raise HTTPException(status_code=404, detail="거래 이력을 찾을 수 없습니다.") if trade.order_type != "SELL" or trade.status not in ("COMPLETED", "FILLED"): raise HTTPException(status_code=400, detail="완료된 매도 거래에 대해서만 일지를 생성할 수 있습니다.") perf = await analyze_performance(db, days=30) content = generate_journal_for_trade(trade, perf) journal = await create_journal_record(db, trade, content) await db.commit() await db.refresh(journal) return { "id": journal.id, "trade_id": journal.trade_id, "stock_code": journal.stock_code, "stock_name": journal.stock_name, "journal_date": _utc_iso(journal.journal_date), "content": journal.content, "strategy_name": journal.strategy_name, "mood": journal.mood, "tags": journal.tags, "created_at": _utc_iso(journal.created_at), } @router.get("/{journal_id}") async def get_journal(journal_id: int, db: AsyncSession = Depends(get_db)): """특정 매매일지 상세 조회""" result = await db.execute(select(TradeJournal).where(TradeJournal.id == journal_id)) journal = result.scalar_one_or_none() if not journal: raise HTTPException(status_code=404, detail="Journal not found") return { "id": journal.id, "trade_id": journal.trade_id, "stock_code": journal.stock_code, "stock_name": journal.stock_name, "journal_date": journal.journal_date.isoformat() if journal.journal_date else None, "content": journal.content, "strategy_name": journal.strategy_name, "mood": journal.mood, "tags": journal.tags, "created_at": journal.created_at.isoformat() if journal.created_at else None, }