115 lines
4.3 KiB
Python
115 lines
4.3 KiB
Python
import json
|
|
import time
|
|
from datetime import date
|
|
from typing import Optional
|
|
|
|
from hydra.logging.setup import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
PNL_REALIZED_KEY = "hydra:pnl:realized:total"
|
|
PNL_TRADE_COUNT_KEY = "hydra:pnl:trade_count"
|
|
|
|
|
|
def _daily_key() -> str:
|
|
return f"hydra:pnl:daily:{date.today().isoformat()}"
|
|
|
|
|
|
class PnlTracker:
|
|
"""
|
|
시스템 전체 손익 추적.
|
|
- 실현 손익: 포지션 청산 시 record_trade()로 기록
|
|
- 미실현 손익: 포지션 데이터(mark_price)로 계산
|
|
"""
|
|
|
|
def __init__(self, redis_client):
|
|
self._redis = redis_client
|
|
|
|
# ─── 실현 손익 ───────────────────────────────────────────────
|
|
|
|
def record_trade(self, market: str, symbol: str, realized_pnl: float) -> None:
|
|
"""포지션 청산 시 실현 손익 기록."""
|
|
pipe = self._redis.pipeline()
|
|
pipe.incrbyfloat(PNL_REALIZED_KEY, realized_pnl)
|
|
pipe.incrbyfloat(_daily_key(), realized_pnl)
|
|
pipe.incr(PNL_TRADE_COUNT_KEY)
|
|
# 개별 심볼 실현 손익
|
|
pipe.incrbyfloat(f"hydra:pnl:symbol:{market}:{symbol}", realized_pnl)
|
|
pipe.execute()
|
|
logger.info("pnl_recorded", market=market, symbol=symbol, realized_pnl=realized_pnl)
|
|
|
|
def get_realized_total(self) -> float:
|
|
raw = self._redis.get(PNL_REALIZED_KEY)
|
|
return float(raw) if raw else 0.0
|
|
|
|
def get_daily_realized(self) -> float:
|
|
raw = self._redis.get(_daily_key())
|
|
return float(raw) if raw else 0.0
|
|
|
|
def get_trade_count(self) -> int:
|
|
raw = self._redis.get(PNL_TRADE_COUNT_KEY)
|
|
return int(raw) if raw else 0
|
|
|
|
def get_symbol_realized(self, market: str, symbol: str) -> float:
|
|
raw = self._redis.get(f"hydra:pnl:symbol:{market}:{symbol}")
|
|
return float(raw) if raw else 0.0
|
|
|
|
# ─── 미실현 손익 ──────────────────────────────────────────────
|
|
|
|
@staticmethod
|
|
def calc_unrealized(position: dict) -> float:
|
|
"""단일 포지션의 미실현 손익 계산.
|
|
position dict: {qty, avg_price, mark_price, side, leverage}
|
|
"""
|
|
qty = float(position.get("qty", 0))
|
|
avg_price = float(position.get("avg_price", 0))
|
|
mark_price = float(position.get("mark_price", avg_price))
|
|
side = position.get("side", "buy")
|
|
leverage = float(position.get("leverage", 1))
|
|
|
|
if qty == 0 or avg_price == 0:
|
|
return 0.0
|
|
|
|
price_diff = mark_price - avg_price
|
|
if side == "sell":
|
|
price_diff = -price_diff
|
|
|
|
return price_diff * qty * leverage
|
|
|
|
def get_unrealized_total(self, positions: list[dict]) -> float:
|
|
return sum(self.calc_unrealized(p) for p in positions)
|
|
|
|
# ─── 요약 ─────────────────────────────────────────────────────
|
|
|
|
def get_summary(self, positions: list[dict]) -> dict:
|
|
realized_total = self.get_realized_total()
|
|
daily_realized = self.get_daily_realized()
|
|
unrealized = self.get_unrealized_total(positions)
|
|
trade_count = self.get_trade_count()
|
|
|
|
return {
|
|
"realized_total": round(realized_total, 4),
|
|
"daily_realized": round(daily_realized, 4),
|
|
"unrealized": round(unrealized, 4),
|
|
"total_pnl": round(realized_total + unrealized, 4),
|
|
"trade_count": trade_count,
|
|
"positions": [
|
|
{
|
|
"market": p.get("market"),
|
|
"symbol": p.get("symbol"),
|
|
"side": p.get("side"),
|
|
"qty": p.get("qty"),
|
|
"avg_price": p.get("avg_price"),
|
|
"mark_price": p.get("mark_price", p.get("avg_price")),
|
|
"leverage": p.get("leverage", 1),
|
|
"unrealized_pnl": round(self.calc_unrealized(p), 4),
|
|
}
|
|
for p in positions
|
|
],
|
|
}
|
|
|
|
def reset_daily(self) -> None:
|
|
"""일일 손익 초기화 (자정 실행용)."""
|
|
self._redis.delete(_daily_key())
|
|
logger.info("pnl_daily_reset")
|