feat: 매매일지 자동 생성 + 전략 자동 최적화 시스템 추가

- trade_analyzer.py: 거래 성과 분석(승률/손익비/전략별 패턴), 템플릿 기반 일지 자동 생성
- strategy_optimizer.py: 그리드 서치 기반 파라미터 최적화, Sharpe 10% 개선 시 자동 적용
- analysis.py: /performance /insights /generate-journals /optimize-strategy /optimization-history 엔드포인트
- journals.py: POST /generate/{trade_id}, GET /stats 엔드포인트 추가
- models.py: StrategyOptimizationLog 테이블 추가
- main.py: post_market_analyzer_scheduler (16:00 KST 자동 실행)
- frontend: Analysis 페이지 추가 (성과 요약/인사이트/일지 생성/최적화 이력)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 09:28:10 +09:00
parent f42e94fe6c
commit 2baf06a5fd
10 changed files with 1789 additions and 5 deletions

View File

@@ -2,11 +2,13 @@
매매일지 API 엔드포인트
- GET /: 최근 AI 생성 매매일지 목록
- GET /stats: 일지 통계 (전략별, 감정 분포)
- GET /{journal_id}: 특정 일지 상세
- POST /generate/{trade_id}: 특정 거래에 대한 일지 즉시 생성
"""
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from sqlalchemy import select, desc, func
from typing import Optional
from datetime import datetime, timezone
import logging
@@ -19,7 +21,7 @@ def _utc_iso(dt: datetime | None) -> str | None:
return dt.replace(tzinfo=timezone.utc).isoformat()
from app.core.database import get_db
from app.models.models import TradeJournal
from app.models.models import TradeJournal, TradeHistory
logger = logging.getLogger(__name__)
@@ -63,6 +65,81 @@ async def list_journals(
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)):
"""특정 매매일지 상세 조회"""