""" Ground Truth 타점·비중·자본 배분 모델 (일반화 명세). 타점 생성(ground_truth.py)과 자본 배분(position_sizing.py)의 공통 언어. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any from config import ( GT_BUY_BB_MAX, GT_BUY_MIN_BARS, GT_BUY_MIN_SWING_PCT, GT_BUY_PCT_LARGE_LEG, GT_BUY_PCT_SMALL_LEG, GT_LARGE_LEG_TOP_PCT, GT_MAX_BUYS_PER_LEG, GT_MAX_SELLS_PER_LEG, GT_MIN_ORDER_KRW, GT_SELL_SPLIT_GAP_PCT, GT_SELECTION_MODE, ) @dataclass(frozen=True) class GtEntrySpec: """ 매수 타점(leg 내 분할) 규칙. Attributes: pivot_kind: ZigZag 저점(trough). price_field: 체결가 = 봉 Low. weight_rule: 저가일수록 큰 비중 (1/price 정규화). max_per_leg: leg당 최대 매수 횟수. min_bars_gap: 분할 매수 최소 봉 간격. """ pivot_kind: str = "trough" price_field: str = "Low" weight_rule: str = "inverse_price_normalized" max_per_leg: int = GT_MAX_BUYS_PER_LEG min_bars_gap: int = GT_BUY_MIN_BARS bb_filter: str = f"bb_pos <= {GT_BUY_BB_MAX}" @dataclass(frozen=True) class GtExitSpec: """ 매도 타점(leg 내 분할) 규칙. Attributes: pivot_kind: major swing 고점(peak). price_field: 체결가 = 봉 High. split_weights: 2회 분할 시 (65%, 35%). split_gap_pct: 2차 고점 인정 최소 괴리(%). """ pivot_kind: str = "peak" price_field: str = "High" split_weights: tuple[float, float] = (0.65, 0.35) split_gap_pct: float = GT_SELL_SPLIT_GAP_PCT max_per_leg: int = GT_MAX_SELLS_PER_LEG @dataclass(frozen=True) class GtCapitalSpec: """ 체결 원화(amount_krw) 배분 규칙. Attributes: buy_formula: 총자산 × 최적매수율, 상한=가용현금. optimal_buy_rate: leg 내 남은 weight 비중. large_leg_top_pct: 수익률 상위 leg 비율. pct_large: 상위 leg 총자산 배분 스케일. pct_small: 그 외 leg 스케일. min_order_krw: 최소 체결 원화. """ buy_formula: str = "min(total_asset * w_share * tier_scale, cash/(1+fee))" optimal_buy_rate: str = "weight / sum(remaining_buy_weights_in_leg)" large_leg_top_pct: float = GT_LARGE_LEG_TOP_PCT pct_large: float = GT_BUY_PCT_LARGE_LEG pct_small: float = GT_BUY_PCT_SMALL_LEG min_order_krw: float = float(GT_MIN_ORDER_KRW) sell_formula: str = "leg_qty * sell_weight * price (last sell = full leg_qty)" @dataclass class GroundTruthModel: """ GT 전체 모델 (타점 + 비중 + 자본). Attributes: selection_mode: 타점 생성 모드. entry: 매수 명세. exit: 매도 명세. capital: 자본 배분 명세. leg: leg 정의. """ selection_mode: str = GT_SELECTION_MODE entry: GtEntrySpec = field(default_factory=GtEntrySpec) exit: GtExitSpec = field(default_factory=GtExitSpec) capital: GtCapitalSpec = field(default_factory=GtCapitalSpec) leg_definition: str = ( "이전 고점 매도 ~ 다음 고점 매도 구간 = leg_id; " "기간말 잔여 구간은 마지막 leg" ) execution_order_chrono: str = ( "amount_krw 배분·summary.pnl_pct = 시각순 체결(매도 후 현금 → 다음 매수 반영)" ) execution_order_leg_block: str = ( "JSON 저장 순서 = leg별 매수 전량 → 매도 전량 (차트·테이블 leg 정합)" ) def default_model() -> GroundTruthModel: """현재 config 기준 GT 모델.""" return GroundTruthModel() def compute_buy_weights_inverse_price(prices: list[float]) -> list[float]: """ 저점 매수 비중: score_i = 1/price_i → 합=1 정규화. Args: prices: leg 내 매수 후보 가격. Returns: weight 리스트 (합 ≈ 1). """ if not prices: return [] scores = [1.0 / max(p, 1e-9) for p in prices] s = sum(scores) return [x / s for x in scores] if s > 0 else [1.0 / len(prices)] * len(prices) def sell_split_weights(n_sells: int) -> list[float]: """ leg 매도 비중. Args: n_sells: 매도 횟수(1 또는 2). Returns: weight 리스트. """ spec = GtExitSpec() if n_sells >= 2: return list(spec.split_weights) return [1.0] def model_to_dict(model: GroundTruthModel | None = None) -> dict[str, Any]: """ JSON·리포트용 모델 dict. Args: model: None이면 default. Returns: 직렬화 dict. """ m = model or default_model() return { "selection_mode": m.selection_mode, "leg_definition": m.leg_definition, "entry": { "pivot": m.entry.pivot_kind, "price": m.entry.price_field, "weight_rule": m.entry.weight_rule, "weight_formula": "w_i = (1/price_i) / sum(1/price_j)", "max_buys_per_leg": m.entry.max_per_leg, "min_bars_between_buys": m.entry.min_bars_gap, "bb_filter": m.entry.bb_filter, }, "exit": { "pivot": m.exit.pivot_kind, "price": m.exit.price_field, "weight_rule": "fixed_split_or_full", "weights_two_sell": list(m.exit.split_weights), "split_gap_pct": m.exit.split_gap_pct, "max_sells_per_leg": m.exit.max_per_leg, }, "capital": { "buy": m.capital.buy_formula, "optimal_buy_rate": m.capital.optimal_buy_rate, "large_leg_top_pct": m.capital.large_leg_top_pct, "pct_large_leg": m.capital.pct_large, "pct_small_leg": m.capital.pct_small, "min_order_krw": m.capital.min_order_krw, "sell": m.capital.sell_formula, }, "execution": { "chrono": m.execution_order_chrono, "leg_block_json": m.execution_order_leg_block, }, } def summarize_leg_weights(trades: list[dict[str, Any]]) -> dict[str, Any]: """ leg별 매수·매도 비중 합 검증용 요약. Args: trades: GT trade dict. Returns: leg_id → {buy_sum, sell_sum, n_buy, n_sell}. """ legs: dict[int, dict[str, Any]] = {} for t in trades: lid = int(t.get("leg_id", 0)) legs.setdefault( lid, {"buy_sum": 0.0, "sell_sum": 0.0, "n_buy": 0, "n_sell": 0}, ) w = float(t.get("weight", 0)) if t.get("action") == "buy": legs[lid]["buy_sum"] += w legs[lid]["n_buy"] += 1 elif t.get("action") == "sell": legs[lid]["sell_sum"] += w legs[lid]["n_sell"] += 1 return legs