""" 매매일지 API 엔드포인트 - GET /: 최근 AI 생성 매매일지 목록 - GET /stats: 일지 통계 (전략별, 감정 분포) - GET /{journal_id}: 특정 일지 상세 - POST /generate/{trade_id}: 특정 거래에 대한 일지 즉시 생성 """ from fastapi import APIRouter, Depends, HTTPException, Query 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}") 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 != "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: from fastapi import HTTPException 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, }