""" 실거래 매수 사이징 — 시뮬(sim_tier_enhanced)과 동일 인과 tier·weight 정책. """ from __future__ import annotations import json from pathlib import Path from typing import Any import pandas as pd from config import ( GT_SIGNAL_CAUSAL, TRADING_FEE_RATE, ) from deepcoin.ground_truth.causal_gt_hybrid import ( _attach_drawdown_to_buys, _bar_index_at, _close_series_from_df, _drawdown_pct_at_index, hybrid_tier_scale, ) from deepcoin.ground_truth.gt_model import leg_entry_weights, remaining_weight_sum from deepcoin.matching.position_sizing import compute_buy_amount_krw from deepcoin.paths import OPS_STATE_DIR LIVE_SIZING_STATE_JSON = OPS_STATE_DIR / "live_sizing_state.json" class LivePositionState: """ 미청산 leg·과거 leg 수익·매수 weight 추적 (시뮬 enrich/causal tier 정합). """ def __init__(self) -> None: """빈 포지션 상태.""" self.current_leg_id: int = 0 self.open_buys: list[dict[str, Any]] = [] self.completed_leg_ret: dict[int, float] = {} self.leg_cost_krw: float = 0.0 self.leg_proceeds_krw: float = 0.0 @classmethod def load(cls, path: Path | None = None) -> LivePositionState: """ 디스크에서 상태 복원. Args: path: JSON 경로. None이면 기본 경로. Returns: LivePositionState 인스턴스. """ p = path or LIVE_SIZING_STATE_JSON st = cls() if not p.is_file(): return st try: data = json.loads(p.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): return st st.current_leg_id = int(data.get("current_leg_id") or 0) st.open_buys = list(data.get("open_buys") or []) st.completed_leg_ret = { int(k): float(v) for k, v in (data.get("completed_leg_ret") or {}).items() } st.leg_cost_krw = float(data.get("leg_cost_krw") or 0.0) st.leg_proceeds_krw = float(data.get("leg_proceeds_krw") or 0.0) return st def save(self, path: Path | None = None) -> None: """ 상태를 디스크에 저장. Args: path: JSON 경로. None이면 기본 경로. """ p = path or LIVE_SIZING_STATE_JSON p.parent.mkdir(parents=True, exist_ok=True) payload = { "current_leg_id": self.current_leg_id, "open_buys": self.open_buys, "completed_leg_ret": self.completed_leg_ret, "leg_cost_krw": round(self.leg_cost_krw, 0), "leg_proceeds_krw": round(self.leg_proceeds_krw, 0), } p.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") def _start_new_leg_if_needed(self) -> None: """포지션 없을 때 새 leg 시작.""" if not self.open_buys: self.current_leg_id += 1 self.leg_cost_krw = 0.0 self.leg_proceeds_krw = 0.0 def record_buy(self, dt: str, price: float, amount_krw: float, fee: float) -> None: """ 체결 매수 기록. Args: dt: 체결 시각. price: 체결가. amount_krw: 매수 원화. fee: 수수료. """ self._start_new_leg_if_needed() self.open_buys.append({"dt": dt, "price": price, "amount_krw": amount_krw}) self.leg_cost_krw += amount_krw + fee def record_sell(self, amount_krw: float, fee: float, *, full_close: bool) -> None: """ 체결 매도 기록. Args: amount_krw: 매도 원화(총액). fee: 수수료. full_close: leg 전량 청산 여부. """ net = amount_krw - fee self.leg_proceeds_krw += net if full_close and self.leg_cost_krw > 0: ret_pct = (self.leg_proceeds_krw - self.leg_cost_krw) / self.leg_cost_krw * 100.0 self.completed_leg_ret[self.current_leg_id] = ret_pct self.open_buys = [] self.leg_cost_krw = 0.0 self.leg_proceeds_krw = 0.0 def plan_buy_amount_krw( self, dt: str, price: float, cash: float, qty: float, df: pd.DataFrame | None = None, *, enhanced: bool = True, fee_rate: float = TRADING_FEE_RATE, ) -> float: """ 시뮬과 동일 tier·weight로 매수 원화 산출. Args: dt: 신호 시각. price: 종가. cash: 가용 원화. qty: 보유 수량. df: OHLC (drawdown). enhanced: conviction·medium tier 사용. fee_rate: 수수료율. Returns: 매수 원화. """ self._start_new_leg_if_needed() prices = [float(b["price"]) for b in self.open_buys] + [price] weights = leg_entry_weights(prices) idx = len(self.open_buys) weight = float(weights[idx]) w_sum = float(sum(weights[idx:])) trade: dict[str, Any] = { "dt": dt, "action": "buy", "price": price, "leg_id": self.current_leg_id, "weight": round(weight, 4), } if df is not None and not df.empty: attached = _attach_drawdown_to_buys([trade], df) if attached: trade = attached[0] from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params dd_params = load_hybrid_dd_params() scale = hybrid_tier_scale( trade, completed_leg_ret=self.completed_leg_ret, enhanced=enhanced, dd_large_pct=dd_params.get("dd_large_pct"), dd_medium_pct=dd_params.get("dd_medium_pct"), ) return compute_buy_amount_krw( cash, qty, price, weight, w_sum, asset_pct_scale=scale, fee_rate=fee_rate, ignore_weight_split=bool(trade.get("conviction_buy")), ) def drawdown_pct_from_df(df: pd.DataFrame, dt: str) -> float: """ bar 시점 drawdown % (인과적). Args: df: DatetimeIndex OHLC. dt: 시각 문자열. Returns: drawdown %. """ if df.empty: return 0.0 close_s = _close_series_from_df(df) bar_idx = _bar_index_at(df, dt) return _drawdown_pct_at_index(close_s, bar_idx) def live_sizing_enabled() -> bool: """실거래 사이징을 시뮬 인과 tier와 정합할지.""" return bool(GT_SIGNAL_CAUSAL)