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:
@@ -112,6 +112,7 @@ def upsert_position(
|
|||||||
amount: float,
|
amount: float,
|
||||||
invested_krw: int,
|
invested_krw: int,
|
||||||
entry_time: str, # ISO 포맷 문자열
|
entry_time: str, # ISO 포맷 문자열
|
||||||
|
trade_id: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""포지션 저장 또는 갱신 (MERGE)."""
|
"""포지션 저장 또는 갱신 (MERGE)."""
|
||||||
sql = """
|
sql = """
|
||||||
@@ -124,9 +125,9 @@ def upsert_position(
|
|||||||
invested_krw = :invested_krw,
|
invested_krw = :invested_krw,
|
||||||
updated_at = SYSTIMESTAMP
|
updated_at = SYSTIMESTAMP
|
||||||
WHEN NOT MATCHED THEN
|
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,
|
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:
|
with _conn() as conn:
|
||||||
conn.cursor().execute(sql, {
|
conn.cursor().execute(sql, {
|
||||||
@@ -136,6 +137,7 @@ def upsert_position(
|
|||||||
"amount": amount,
|
"amount": amount,
|
||||||
"invested_krw": invested_krw,
|
"invested_krw": invested_krw,
|
||||||
"entry_time": entry_time,
|
"entry_time": entry_time,
|
||||||
|
"trade_id": trade_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -170,12 +172,37 @@ def ensure_trade_results_table() -> None:
|
|||||||
raise
|
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:
|
with _conn() as conn:
|
||||||
conn.cursor().execute(
|
conn.cursor().execute(
|
||||||
"INSERT INTO trade_results (ticker, is_win, pnl_pct) VALUES (:t, :w, :p)",
|
"INSERT INTO trade_results "
|
||||||
{"t": ticker, "w": 1 if is_win else 0, "p": round(pnl_pct, 4)},
|
"(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 = """
|
sql = """
|
||||||
SELECT ticker, buy_price, peak_price, amount, invested_krw,
|
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
|
FROM positions
|
||||||
"""
|
"""
|
||||||
with _conn() as conn:
|
with _conn() as conn:
|
||||||
@@ -265,6 +293,7 @@ def load_positions() -> list[dict]:
|
|||||||
"amount": float(r[3]),
|
"amount": float(r[3]),
|
||||||
"invested_krw": int(r[4]),
|
"invested_krw": int(r[4]),
|
||||||
"entry_time": r[5],
|
"entry_time": r[5],
|
||||||
|
"trade_id": r[6] or "",
|
||||||
}
|
}
|
||||||
for r in rows
|
for r in rows
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -60,7 +61,13 @@ def _get_history(ticker: str) -> list[bool]:
|
|||||||
return _trade_history[ticker]
|
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 기록."""
|
"""매도 후 in-memory 이력 갱신 + DB 기록."""
|
||||||
hist = _trade_history.setdefault(ticker, [])
|
hist = _trade_history.setdefault(ticker, [])
|
||||||
hist.append(is_win)
|
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:
|
if len(hist) > WF_WINDOW * 2:
|
||||||
_trade_history[ticker] = hist[-WF_WINDOW:]
|
_trade_history[ticker] = hist[-WF_WINDOW:]
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
logger.error(f"거래 이력 저장 실패 {ticker}: {e}")
|
logger.error(f"거래 이력 저장 실패 {ticker}: {e}")
|
||||||
|
|
||||||
@@ -83,6 +93,7 @@ def _db_upsert(ticker: str, pos: dict) -> None:
|
|||||||
amount=pos["amount"],
|
amount=pos["amount"],
|
||||||
invested_krw=pos["invested_krw"],
|
invested_krw=pos["invested_krw"],
|
||||||
entry_time=pos["entry_time"].isoformat(),
|
entry_time=pos["entry_time"].isoformat(),
|
||||||
|
trade_id=pos.get("trade_id", ""),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"포지션 DB 저장 실패 {ticker}: {e}")
|
logger.error(f"포지션 DB 저장 실패 {ticker}: {e}")
|
||||||
@@ -158,6 +169,7 @@ def restore_positions() -> None:
|
|||||||
"amount": amount,
|
"amount": amount,
|
||||||
"invested_krw": s["invested_krw"],
|
"invested_krw": s["invested_krw"],
|
||||||
"entry_time": entry_time,
|
"entry_time": entry_time,
|
||||||
|
"trade_id": s.get("trade_id", ""),
|
||||||
}
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[복원] {ticker} 매수가={s['buy_price']:,.0f}원 | 현재가={current:,.0f}원 "
|
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)
|
actual_price = order_krw / amount if amount > 0 else pyupbit.get_current_price(ticker)
|
||||||
|
|
||||||
entry_time = datetime.now()
|
entry_time = datetime.now()
|
||||||
|
trade_id = str(uuid.uuid4())
|
||||||
_positions[ticker] = {
|
_positions[ticker] = {
|
||||||
"buy_price": actual_price,
|
"buy_price": actual_price,
|
||||||
"peak_price": actual_price,
|
"peak_price": actual_price,
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
"invested_krw": order_krw,
|
"invested_krw": order_krw,
|
||||||
"entry_time": entry_time,
|
"entry_time": entry_time,
|
||||||
|
"trade_id": trade_id,
|
||||||
}
|
}
|
||||||
_db_upsert(ticker, _positions[ticker])
|
_db_upsert(ticker, _positions[ticker])
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[매수] {ticker} @ {actual_price:,.0f}원 (실체결가) | "
|
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)
|
notify_buy(ticker, actual_price, amount, order_krw)
|
||||||
return True
|
return True
|
||||||
@@ -318,7 +332,14 @@ def sell(ticker: str, reason: str = "") -> bool:
|
|||||||
upsert_sell_price(ticker, current) # DB 영구 저장
|
upsert_sell_price(ticker, current) # DB 영구 저장
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"직전 매도가 DB 저장 실패 {ticker}: {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]
|
del _positions[ticker]
|
||||||
try:
|
try:
|
||||||
delete_position(ticker)
|
delete_position(ticker)
|
||||||
|
|||||||
Reference in New Issue
Block a user