Initial public release
This commit is contained in:
114
hydra/core/pnl_tracker.py
Normal file
114
hydra/core/pnl_tracker.py
Normal file
@@ -0,0 +1,114 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user