Files
Bithumb/deepcoin/ground_truth/gt_model.py
dsyoon 5842cc9fa3 GT 총자산 비율 매수·leg 티어 배분과 시뮬/실거래 포지션 사이징을 통합한다.
타점·비중을 gt_model로 일반화하고, amount_krw 시각순 배분·EV/WF·상위 leg 대형 매수를
position_sizing과 시뮬 HTML(고정 ₩/회 비교)에 반영한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 16:11:49 +09:00

229 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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