bf041e4d18
- order_executor: _update_trade_exit에 pnl 계산 저장 추가 - main: 매수 시 positions DB INSERT, 매도 시 DELETE - main: 재시작 시 DB에서 positions 복원 (_restore_positions_from_db)
109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
"""
|
|
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,
|
|
))
|