feat: add trade_id + full trade record to trade_results

Each buy generates a UUID trade_id stored in positions table.
Each sell links via same trade_id in trade_results, enabling
round-trip grouping of buy→sell pairs.

Additional fields saved per trade:
- fee_krw: commission amount (0.05% each side)
- krw_profit: net KRW profit/loss after fees
- buy_price / sell_price: exact prices
- invested_krw: capital deployed
- sell_reason: trailing stop / time stop / etc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-01 05:54:06 +09:00
parent 60e739d18b
commit bcef128155
2 changed files with 61 additions and 11 deletions

View File

@@ -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)