""" GT 체결 amount_krw·총자산 비율 분석 — 시뮬 tier·배분율 최적 추정. """ from __future__ import annotations from typing import Any from config import ( GT_BUY_PCT_LARGE_LEG, GT_BUY_PCT_SMALL_LEG, GT_INITIAL_CASH_KRW, GT_LARGE_LEG_TOP_PCT, TRADING_FEE_RATE, ) from deepcoin.matching.position_sizing import ( leg_asset_pct_scale, optimal_weight_share, portfolio_totals, top_leg_ids_by_forward_return, ) def analyze_gt_buy_allocation( trades: list[dict[str, Any]], *, initial_cash: float = GT_INITIAL_CASH_KRW, fee_rate: float = TRADING_FEE_RATE, ) -> dict[str, Any]: """ GT 시각순 체결에서 매수별 (실제투입/총자산) 비율을 분석합니다. Args: trades: amount_krw·weight·leg_id가 채워진 GT trade dict. initial_cash: 시작 현금. fee_rate: 수수료율. Returns: leg tier별·전체 배분 통계 및 권장 pct_large/pct_small. """ chron = sorted(trades, key=lambda x: x["dt"]) if not chron: return {"note": "체결 없음"} large_legs = top_leg_ids_by_forward_return(chron, GT_LARGE_LEG_TOP_PCT) cash = float(initial_cash) qty = 0.0 ratios_large: list[float] = [] ratios_small: list[float] = [] ratios_all: list[float] = [] for i, t in enumerate(chron): price = float(t["price"]) if price <= 0: continue leg_id = int(t.get("leg_id", 0)) action = t.get("action", "") if action == "buy": w = float(t.get("weight", 1.0)) rem = sum( float(chron[j].get("weight", 1.0)) for j in range(i, len(chron)) if int(chron[j].get("leg_id", 0)) == leg_id and chron[j].get("action") == "buy" ) opt = optimal_weight_share(w, rem) if rem > 0 else 1.0 total_asset, _, _ = portfolio_totals(cash, qty, price) amount = float(t.get("amount_krw") or 0) if total_asset > 0 and amount > 0 and opt > 0: implied = amount / (total_asset * opt) ratios_all.append(implied) if leg_id in large_legs: ratios_large.append(implied) else: ratios_small.append(implied) if amount > 0: fee = amount * fee_rate cash -= amount + fee qty += amount / price elif action == "sell" and qty > 0: gross = float(t.get("amount_krw") or qty * price) cash += gross * (1.0 - fee_rate) qty = 0.0 def _stats(vals: list[float]) -> dict[str, float]: if not vals: return {} s = sorted(vals) n = len(s) return { "count": n, "mean": round(sum(s) / n, 4), "median": round(s[n // 2], 4), "p25": round(s[max(0, n // 4)], 4), "p75": round(s[min(n - 1, 3 * n // 4)], 4), } st_all = _stats(ratios_all) st_large = _stats(ratios_large) st_small = _stats(ratios_small) rec_large = st_large.get("median", GT_BUY_PCT_LARGE_LEG) rec_small = st_small.get("median", GT_BUY_PCT_SMALL_LEG) if not rec_large or rec_large <= 0: rec_large = GT_BUY_PCT_LARGE_LEG if not rec_small or rec_small <= 0: rec_small = GT_BUY_PCT_SMALL_LEG return { "large_leg_ids": sorted(large_legs), "large_leg_count": len(large_legs), "config_pct_large": GT_BUY_PCT_LARGE_LEG, "config_pct_small": GT_BUY_PCT_SMALL_LEG, "observed_implied_scale": { "all": st_all, "large_leg": st_large, "small_leg": st_small, }, "recommended_pct_large_leg": round(rec_large, 4), "recommended_pct_small_leg": round(rec_small, 4), "note": ( "implied_scale = amount / (pre_buy_total_asset × weight_share); " "시뮬 tier는 GT 분석 median 사용" ), } def gt_tier_scale_from_analysis( leg_id: int, large_legs: set[int], analysis: dict[str, Any] | None = None, ) -> float: """ GT 분석 권장값 또는 config tier scale. Args: leg_id: leg 번호. large_legs: 상위 leg. analysis: analyze_gt_buy_allocation 결과. Returns: 총자산 대비 매수 스케일 (0~1). """ if analysis and analysis.get("observed_implied_scale", {}).get("all"): if leg_id in large_legs: return float(analysis.get("recommended_pct_large_leg", GT_BUY_PCT_LARGE_LEG)) return float(analysis.get("recommended_pct_small_leg", GT_BUY_PCT_SMALL_LEG)) return leg_asset_pct_scale(leg_id, large_legs)