""" execution/order_executor.py 주문 실행 모듈 DRY_RUN=true 시 실제 주문 전송 없음 """ import os import asyncio import logging from datetime import datetime from app.execution.kis_client import KISClient from app.db.models import get_conn from app.config import FEE_RATE, TAX_RATE logger = logging.getLogger(__name__) class OrderExecutor: def __init__(self, kis: KISClient): self.kis = kis self.dry_run = os.getenv("DRY_RUN", "true").lower() == "true" def _calc_fee(self, price: float, qty: int, is_buy: bool) -> float: amt = price * qty return amt * (FEE_RATE + (0 if is_buy else TAX_RATE)) async def buy(self, ticker: str, name: str, qty: int, reason: str = "", ai_boosted: bool = False) -> dict: """시장가 매수""" try: result = await self.kis.order_buy(ticker, qty) price = result.get("entry_price", 0) # DB 저장 fee = self._calc_fee(price, qty, True) self._save_trade( ticker=ticker, name=name, entry_price=price, qty=qty, side="BUY", fee=fee, ai_boosted=ai_boosted, ) mode = "[DRY]" if self.dry_run else "" logger.info(f"{mode} 매수 {name}({ticker}) {qty}주 @ {price:,}원") return {"success": True, "price": price, "qty": qty} except Exception as e: logger.error(f"매수 실패 {ticker}: {e}") return {"success": False, "error": str(e)} async def sell(self, ticker: str, name: str, qty: int, reason: str = "") -> dict: """시장가 매도""" try: result = await self.kis.order_sell(ticker, qty) price = result.get("exit_price", 0) fee = self._calc_fee(price, qty, False) self._update_trade_exit( ticker=ticker, exit_price=price, qty=qty, reason=reason, fee=fee, ) mode = "[DRY]" if self.dry_run else "" logger.info(f"{mode} 매도 {name}({ticker}) {qty}주 @ {price:,}원 [{reason}]") return {"success": True, "price": price, "qty": qty} except Exception as e: logger.error(f"매도 실패 {ticker}: {e}") return {"success": False, "error": str(e)} def _save_trade(self, ticker, name, entry_price, qty, side, fee, ai_boosted=False): with get_conn() as conn: conn.execute(""" INSERT INTO trades (date, ticker, name, entry_time, entry_price, quantity, side, fee, ai_boosted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( datetime.now().strftime("%Y-%m-%d"), ticker, name, datetime.now().strftime("%H:%M:%S"), entry_price, qty, side, fee, 1 if ai_boosted else 0, )) def _update_trade_exit(self, ticker, exit_price, qty, reason, fee): with get_conn() as conn: row = conn.execute(""" SELECT id, entry_price, quantity FROM trades WHERE ticker=? AND exit_time IS NULL ORDER BY id DESC LIMIT 1 """, (ticker,)).fetchone() if not row: return trade_id, entry_price, trade_qty = row actual_qty = qty if qty else trade_qty pnl = (exit_price - entry_price) * actual_qty - fee conn.execute(""" UPDATE trades SET exit_time=?, exit_price=?, exit_reason=?, fee=fee+?, pnl=? WHERE id=? """, ( datetime.now().strftime("%H:%M:%S"), exit_price, reason, fee, pnl, trade_id, ))