수정본
This commit is contained in:
@@ -1,15 +1,24 @@
|
||||
"""
|
||||
주문/체결 API 엔드포인트
|
||||
|
||||
- GET /: 거래 이력 조회 (손익 포함)
|
||||
- GET /stats/summary: 거래 통계
|
||||
- DELETE /pending: PENDING/FAILED 거래 정리
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.models import TradeHistory
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TradeHistoryResponse(BaseModel):
|
||||
@@ -22,11 +31,17 @@ class TradeHistoryResponse(BaseModel):
|
||||
order_quantity: int
|
||||
executed_price: Optional[Decimal]
|
||||
executed_quantity: Optional[int]
|
||||
total_amount: Optional[Decimal]
|
||||
status: str
|
||||
order_id: Optional[str]
|
||||
message: Optional[str]
|
||||
entry_reason: Optional[str]
|
||||
exit_reason: Optional[str]
|
||||
signals: Optional[List[str]]
|
||||
created_at: datetime
|
||||
executed_at: Optional[datetime]
|
||||
profit_loss: Optional[Decimal] = None
|
||||
profit_loss_rate: Optional[float] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -42,7 +57,7 @@ async def get_trade_history(
|
||||
days: int = 30,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""거래 이력 조회"""
|
||||
"""거래 이력 조회 (손익 포함)"""
|
||||
query = select(TradeHistory)
|
||||
|
||||
# 날짜 필터
|
||||
@@ -66,7 +81,64 @@ async def get_trade_history(
|
||||
|
||||
result = await db.execute(query)
|
||||
trades = result.scalars().all()
|
||||
return trades
|
||||
|
||||
# 손익 계산을 위해 매수/매도 매칭
|
||||
trade_responses = []
|
||||
buy_trades = {} # stock_code -> list of buy trades
|
||||
|
||||
for trade in trades:
|
||||
response = TradeHistoryResponse.model_validate(trade)
|
||||
|
||||
# 매도 시 손익 정보 설정
|
||||
if trade.order_type == "SELL" and trade.status == "EXECUTED":
|
||||
import re
|
||||
|
||||
# 1. DB에 저장된 값 먼저 사용
|
||||
if hasattr(trade, 'net_profit_loss') and trade.net_profit_loss is not None:
|
||||
response.profit_loss = trade.net_profit_loss
|
||||
logger.debug(f"✅ DB에서 손익 금액 로드: {response.profit_loss}")
|
||||
|
||||
if hasattr(trade, 'profit_loss_rate') and trade.profit_loss_rate is not None:
|
||||
response.profit_loss_rate = float(trade.profit_loss_rate)
|
||||
logger.debug(f"✅ DB에서 손익률 로드: {response.profit_loss_rate}%")
|
||||
|
||||
# 2. DB 값이 없으면 exit_reason/message에서 파싱 (이전 데이터 호환성)
|
||||
if response.profit_loss is None or response.profit_loss_rate is None:
|
||||
text_to_parse = trade.exit_reason or trade.message or ""
|
||||
logger.debug(f"🔍 손익 파싱 시도 - 종목: {trade.stock_name}, 텍스트: {text_to_parse[:100]}")
|
||||
|
||||
# 손익 금액 추출
|
||||
if response.profit_loss is None:
|
||||
profit_match = re.search(r'손익:\s*([+-]?[\d,]+)원', text_to_parse)
|
||||
if profit_match:
|
||||
profit_str = profit_match.group(1).replace(',', '')
|
||||
try:
|
||||
response.profit_loss = Decimal(profit_str)
|
||||
logger.info(f"✅ 파싱으로 손익 금액 추출: {response.profit_loss}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 손익 금액 변환 실패: {e}")
|
||||
|
||||
# 손익률 추출
|
||||
if response.profit_loss_rate is None:
|
||||
message_rate_match = re.search(r'손익률\s*([+-]?[\d.]+)%', trade.message or "")
|
||||
if message_rate_match:
|
||||
try:
|
||||
response.profit_loss_rate = float(message_rate_match.group(1))
|
||||
logger.info(f"✅ 파싱으로 손익률 추출 (message): {response.profit_loss_rate}%")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 손익률 변환 실패: {e}")
|
||||
else:
|
||||
rate_match = re.search(r'([+-]?[\d.]+)%', text_to_parse)
|
||||
if rate_match:
|
||||
try:
|
||||
response.profit_loss_rate = float(rate_match.group(1))
|
||||
logger.info(f"✅ 파싱으로 손익률 추출 (exit_reason): {response.profit_loss_rate}%")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 손익률 변환 실패: {e}")
|
||||
|
||||
trade_responses.append(response)
|
||||
|
||||
return trade_responses
|
||||
|
||||
|
||||
@router.get("/{trade_id}", response_model=TradeHistoryResponse)
|
||||
@@ -116,3 +188,23 @@ async def get_trade_stats(
|
||||
"success_rate": executed_trades / total_trades if total_trades > 0 else 0,
|
||||
"total_amount": total_amount
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/pending")
|
||||
async def delete_pending_trades(db: AsyncSession = Depends(get_db)):
|
||||
"""장 마감 후 PENDING과 FAILED 상태의 매수 시도 내역 정리"""
|
||||
result = await db.execute(
|
||||
select(TradeHistory).where(
|
||||
TradeHistory.status.in_(['PENDING', 'FAILED'])
|
||||
)
|
||||
)
|
||||
pending_trades = result.scalars().all()
|
||||
count = len(pending_trades)
|
||||
|
||||
for trade in pending_trades:
|
||||
await db.delete(trade)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"✅ 정리 완료: {count}개의 PENDING/FAILED 매수 시도 삭제")
|
||||
|
||||
return {"deleted_count": count, "message": f"Cleaned up {count} pending/failed trades"}
|
||||
|
||||
Reference in New Issue
Block a user