diff --git a/core/price_db.py b/core/price_db.py index e3bca77..2c17a01 100644 --- a/core/price_db.py +++ b/core/price_db.py @@ -112,6 +112,7 @@ def upsert_position( amount: float, invested_krw: int, entry_time: str, # ISO 포맷 문자열 + trade_id: str = "", ) -> None: """포지션 저장 또는 갱신 (MERGE).""" sql = """ @@ -124,9 +125,9 @@ def upsert_position( invested_krw = :invested_krw, updated_at = SYSTIMESTAMP WHEN NOT MATCHED THEN - INSERT (ticker, buy_price, peak_price, amount, invested_krw, entry_time) + INSERT (ticker, buy_price, peak_price, amount, invested_krw, entry_time, trade_id) VALUES (:ticker, :buy_price, :peak_price, :amount, :invested_krw, - TO_TIMESTAMP(:entry_time, 'YYYY-MM-DD"T"HH24:MI:SS.FF6')) + TO_TIMESTAMP(:entry_time, 'YYYY-MM-DD"T"HH24:MI:SS.FF6'), :trade_id) """ with _conn() as conn: conn.cursor().execute(sql, { @@ -136,6 +137,7 @@ def upsert_position( "amount": amount, "invested_krw": invested_krw, "entry_time": entry_time, + "trade_id": trade_id, }) @@ -170,12 +172,37 @@ def ensure_trade_results_table() -> None: raise -def record_trade(ticker: str, is_win: bool, pnl_pct: float) -> None: - """거래 결과 저장.""" +def record_trade( + ticker: str, + is_win: bool, + pnl_pct: float, + fee_krw: float = 0.0, + krw_profit: float = 0.0, + trade_id: str = "", + buy_price: float = 0.0, + sell_price: float = 0.0, + invested_krw: int = 0, + sell_reason: str = "", +) -> None: + """거래 결과 저장 (수수료·KRW 손익·trade_id 포함).""" with _conn() as conn: conn.cursor().execute( - "INSERT INTO trade_results (ticker, is_win, pnl_pct) VALUES (:t, :w, :p)", - {"t": ticker, "w": 1 if is_win else 0, "p": round(pnl_pct, 4)}, + "INSERT INTO trade_results " + "(ticker, is_win, pnl_pct, fee_krw, krw_profit, " + " trade_id, buy_price, sell_price, invested_krw, sell_reason) " + "VALUES (:t, :w, :p, :f, :k, :tid, :bp, :sp, :ikrw, :sr)", + { + "t": ticker, + "w": 1 if is_win else 0, + "p": round(pnl_pct, 4), + "f": round(fee_krw, 2), + "k": round(krw_profit, 2), + "tid": trade_id, + "bp": buy_price, + "sp": sell_price, + "ikrw": invested_krw, + "sr": sell_reason, + }, ) @@ -250,7 +277,8 @@ def load_positions() -> list[dict]: """저장된 전체 포지션 로드.""" sql = """ SELECT ticker, buy_price, peak_price, amount, invested_krw, - TO_CHAR(entry_time, 'YYYY-MM-DD"T"HH24:MI:SS.FF6') AS entry_time + TO_CHAR(entry_time, 'YYYY-MM-DD"T"HH24:MI:SS.FF6') AS entry_time, + trade_id FROM positions """ with _conn() as conn: @@ -265,6 +293,7 @@ def load_positions() -> list[dict]: "amount": float(r[3]), "invested_krw": int(r[4]), "entry_time": r[5], + "trade_id": r[6] or "", } for r in rows ] diff --git a/core/trader.py b/core/trader.py index fa81a24..fabb9f4 100644 --- a/core/trader.py +++ b/core/trader.py @@ -6,6 +6,7 @@ import logging import os import threading import time +import uuid from datetime import datetime from typing import Optional @@ -60,7 +61,13 @@ def _get_history(ticker: str) -> list[bool]: return _trade_history[ticker] -def _update_history(ticker: str, is_win: bool, pnl_pct: float) -> None: +def _update_history( + ticker: str, is_win: bool, pnl_pct: float, + fee_krw: float = 0.0, krw_profit: float = 0.0, + trade_id: str = "", buy_price: float = 0.0, + sell_price: float = 0.0, invested_krw: int = 0, + sell_reason: str = "", +) -> None: """매도 후 in-memory 이력 갱신 + DB 기록.""" hist = _trade_history.setdefault(ticker, []) hist.append(is_win) @@ -68,7 +75,10 @@ def _update_history(ticker: str, is_win: bool, pnl_pct: float) -> None: if len(hist) > WF_WINDOW * 2: _trade_history[ticker] = hist[-WF_WINDOW:] try: - record_trade(ticker, is_win, pnl_pct) + record_trade( + ticker, is_win, pnl_pct, fee_krw, krw_profit, + trade_id, buy_price, sell_price, invested_krw, sell_reason, + ) except Exception as e: logger.error(f"거래 이력 저장 실패 {ticker}: {e}") @@ -83,6 +93,7 @@ def _db_upsert(ticker: str, pos: dict) -> None: amount=pos["amount"], invested_krw=pos["invested_krw"], entry_time=pos["entry_time"].isoformat(), + trade_id=pos.get("trade_id", ""), ) except Exception as e: logger.error(f"포지션 DB 저장 실패 {ticker}: {e}") @@ -158,6 +169,7 @@ def restore_positions() -> None: "amount": amount, "invested_krw": s["invested_krw"], "entry_time": entry_time, + "trade_id": s.get("trade_id", ""), } logger.info( f"[복원] {ticker} 매수가={s['buy_price']:,.0f}원 | 현재가={current:,.0f}원 " @@ -252,17 +264,19 @@ def buy(ticker: str) -> bool: actual_price = order_krw / amount if amount > 0 else pyupbit.get_current_price(ticker) entry_time = datetime.now() + trade_id = str(uuid.uuid4()) _positions[ticker] = { "buy_price": actual_price, "peak_price": actual_price, "amount": amount, "invested_krw": order_krw, "entry_time": entry_time, + "trade_id": trade_id, } _db_upsert(ticker, _positions[ticker]) logger.info( f"[매수] {ticker} @ {actual_price:,.0f}원 (실체결가) | " - f"수량={amount} | 투자금={order_krw:,}원" + f"수량={amount} | 투자금={order_krw:,}원 | trade_id={trade_id[:8]}" ) notify_buy(ticker, actual_price, amount, order_krw) return True @@ -318,7 +332,14 @@ def sell(ticker: str, reason: str = "") -> bool: upsert_sell_price(ticker, current) # DB 영구 저장 except Exception as e: logger.error(f"직전 매도가 DB 저장 실패 {ticker}: {e}") - _update_history(ticker, pnl > 0, pnl) # walk-forward 이력 갱신 + _update_history( + ticker, pnl > 0, pnl, fee, krw_profit, + trade_id=pos.get("trade_id", ""), + buy_price=pos["buy_price"], + sell_price=current or pos["buy_price"], + invested_krw=pos["invested_krw"], + sell_reason=reason, + ) del _positions[ticker] try: delete_position(ticker)