## CRITICAL 수정 (20건) - day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가 - trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산 - risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수 - risk_management/recommendations: date.today() → KST 변환 - mdd_manager: Lock 밖 executed_key 참조 수정 - correlation_filter: output → output2 키 수정 - portfolio: 해외 종목 USD/KRW 환율 적용 분리 - Sharpe Ratio 거래빈도 기반 연간화 - telegram_notifier: send_message() 공개 래퍼 추가 - position_manager: 앱 재시작 시 DB 포지션 복원 - swing exit_service: remaining_pct 조건 범위 수정 - run_backtest: original_qty 기반 remaining_pct 계산 - us_market_schedule: 2027년 미국 공휴일 추가 - Login.tsx: res.ok 검증 추가 - mean_reversion_strategy: RSI 로그 하드코딩 수정 ## HIGH 수정 (10건) - db_worker: DLQ 파일 백업 메커니즘 추가 - korea_investment_api: 만료 토큰 임의 연장 제거 - market_data: chart_cache 최대 100개 + TTL 만료 - main.py: rate_limit_store 1시간 미접근 IP 정리 - recommendations: fire-and-forget try/except 추가 - swing/scheduler: DB 세션 3개 분리 - Dashboard: wsMessages 전체 순회 - Layout: /health 엔드포인트 + 30초 폴링 - ml_ensemble: POST /train require_api_key 추가 - day_trading: record_trade_result_safe() 전환 ## 코드 정리 - 프론트엔드: isUsStock, formatTime 공통 유틸 추출 - 백엔드: dead code 제거, 중복 import 통합 - MCP: JSON 파싱 헬퍼 추출 (~60줄 감소) - 전체 ~100개 파일 한국어 주석 추가 - 테스트 13건 코드 기준 수정 (468 passed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
162 lines
5.6 KiB
Python
162 lines
5.6 KiB
Python
"""
|
|
매매일지 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 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,
|
|
}
|