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:
@@ -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)):
|
||||
"""특정 매매일지 상세 조회"""
|
||||
|
||||
Reference in New Issue
Block a user