인과 GT leg 엔진·drawdown tier·train 캘리브레이션, Phase 2 Go/No-Go 및 시뮬 리포트를 반영한다. Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
6.5 KiB
Python
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)
|