Files
Bithumb/deepcoin/matching/live_sizing.py
xavis d385456867 hybrid DD tier와 Option C 2차(+1000%) 검증을 추가하고 실거래 사이징을 정합한다.
인과 GT leg 엔진·drawdown tier·train 캘리브레이션, Phase 2 Go/No-Go 및 시뮬 리포트를 반영한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 16:09:28 +09:00

215 lines
6.5 KiB
Python

"""
실거래 매수 사이징 — 시뮬(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)