Initial public release

This commit is contained in:
sinmb79
2026-03-30 13:19:11 +09:00
commit 92a692b63c
116 changed files with 5822 additions and 0 deletions

114
hydra/core/pnl_tracker.py Normal file
View 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")