""" 규칙 발화 기반 GT 모델 복리 포트폴리오 시뮬. """ from __future__ import annotations from typing import Any import pandas as pd from config import ( GT_INITIAL_CASH_KRW, GT_SIGNAL_CAUSAL, LIVE_ORDER_KRW, TRADING_FEE_RATE, ) from deepcoin.ground_truth.gt_allocation import simulate_portfolio_summary from deepcoin.matching.position_sizing import ( attach_gt_model_amounts, ) def _planned_order_krw( t: dict[str, Any], order_krw: float, sizing_mode: str, ) -> float: """ 체결 계획 원화: amount_krw 우선 또는 고정. Args: t: trade dict. order_krw: 고정 1회 금액. sizing_mode: fixed | amount_krw. Returns: 계획 원화. """ ak = t.get("amount_krw") if sizing_mode == "amount_krw" or (ak is not None and float(ak) > 0): return float(ak or 0) return float(order_krw) def sort_fires_chronological(fires: pd.DataFrame) -> pd.DataFrame: """ 발화를 시간순 정렬 (일·금액 한도 없음). Args: fires: fire_outcomes. Returns: 정렬된 DataFrame. """ if fires.empty: return fires return fires.sort_values("dt").copy() def simulate_fires_compound( fires: pd.DataFrame, *, initial_cash: float = GT_INITIAL_CASH_KRW, fee_rate: float = TRADING_FEE_RATE, ) -> tuple[list[dict[str, Any]], dict[str, Any]]: """ 발화 → GT tier 복리 amount_krw 배분 (allocate_order_amounts_chronological). Args: fires: fire_outcomes. initial_cash: 시작 현금 (이후 체결마다 누적). fee_rate: 수수료율. Returns: (amount_krw 채워진 trade dict, stats). """ trades = fires_to_trade_list( fires, apply_dynamic_sizing=True, initial_cash=initial_cash, fee_rate=fee_rate, ) n_in = int(len(fires)) n_out = sum(1 for t in trades if float(t.get("amount_krw") or 0) > 0) return trades, { "input_fires": n_in, "executed": n_out, "skipped": max(n_in - n_out, 0), } def select_capped_fires( fires: pd.DataFrame, *, use_dynamic_sizing: bool = True, ) -> pd.DataFrame: """ 시각순 발화 반환 (레거시명; 일·금액 한도 미적용). Args: fires: fire_outcomes. use_dynamic_sizing: 미사용 (하위 호환). Returns: 정렬된 발화 DataFrame. """ _ = use_dynamic_sizing return sort_fires_chronological(fires) def fires_to_trade_list( fires: pd.DataFrame, *, apply_dynamic_sizing: bool = True, initial_cash: float = GT_INITIAL_CASH_KRW, fee_rate: float = TRADING_FEE_RATE, ) -> list[dict[str, Any]]: """ 발화 → GT 모델 amount_krw가 채워진 trade dict (복리 배분). Args: fires: 체결 대상 발화. apply_dynamic_sizing: True면 GT tier 복리 배분. initial_cash: 시작 현금 (누적 복리). fee_rate: 수수료율. Returns: dt, action, price, amount_krw 키 dict 리스트. """ if fires.empty: return [] rows: list[dict[str, Any]] = [] for _, r in sort_fires_chronological(fires).iterrows(): rows.append( { "dt": str(r["dt"]), "action": r["side"], "price": float(r["close"]), "rule_id": r.get("rule_id", ""), "forward_ret_pct": float(r.get("forward_ret_pct", 0)), } ) if apply_dynamic_sizing: attach_gt_model_amounts( rows, initial_cash=initial_cash, fee_rate=fee_rate, ) return rows def simulate_sized_portfolio( trades: list[dict[str, Any]], initial_cash: float = GT_INITIAL_CASH_KRW, fee_rate: float = TRADING_FEE_RATE, last_price: float | None = None, fallback_order_krw: float = LIVE_ORDER_KRW, ) -> dict[str, Any]: """ trade.amount_krw(GT 모델·복리 배분) 기준 포트폴리오 시뮬 + MDD. Args: trades: 시간순 trade dict (amount_krw 권장). initial_cash: 시작 현금. fee_rate: 수수료율. last_price: 미청산 평가 종가. fallback_order_krw: amount_krw 없을 때 1회 금액. Returns: simulate_truth_portfolio와 동일 키 + max_drawdown_pct. """ if trades and not any(float(t.get("amount_krw") or 0) > 0 for t in trades): attach_gt_model_amounts(trades, initial_cash=initial_cash, fee_rate=fee_rate) result = simulate_portfolio_summary( trades, initial_cash=initial_cash, fee_rate=fee_rate, last_price=last_price, use_amount_krw=True, ) result["sizing_mode"] = ( "gt_model_compound_causal" if GT_SIGNAL_CAUSAL else "gt_model_compound" ) result["sizing_note"] = ( "전기간 복리·GT tier·총자산×비중, 보유현금 한도; " + ("인과적 신호·tier(미래 미사용)" if GT_SIGNAL_CAUSAL else "상한 없음") ) return result def simulate_fixed_order_portfolio( trades: list[dict[str, Any]], order_krw: float = LIVE_ORDER_KRW, initial_cash: float = GT_INITIAL_CASH_KRW, fee_rate: float = TRADING_FEE_RATE, last_price: float | None = None, sizing_mode: str = "fixed", ) -> dict[str, Any]: """ 포트폴리오 시뮬 (고정 원화 또는 trade.amount_krw). Args: trades: 시간순 {dt, action, price, amount_krw?}. order_krw: sizing_mode=fixed 일 때 1회 금액(원). initial_cash: 시작 현금. fee_rate: 수수료율. last_price: 미청산 평가 종가. sizing_mode: 'fixed' | 'amount_krw'. Returns: simulate_truth_portfolio와 동일 키 구조. """ cash = float(initial_cash) qty = 0.0 total_fees = 0.0 last_trade_price = last_price order = float(order_krw) for t in sorted(trades, key=lambda x: x["dt"]): action = t["action"] price = float(t["price"]) if price <= 0: continue last_trade_price = price if action == "buy": planned = _planned_order_krw(t, order, sizing_mode) amount = min(planned, max(cash / (1.0 + fee_rate), 0.0)) if amount <= 0: continue fee = amount * fee_rate cash -= amount + fee total_fees += fee qty += amount / price elif action == "sell" and qty > 0: planned = _planned_order_krw(t, order, sizing_mode) if planned >= qty * price * 0.999: sell_qty = qty else: sell_qty = min(qty, planned / price) if sell_qty <= 0: continue gross = sell_qty * price fee = gross * fee_rate cash += gross - fee total_fees += fee qty -= sell_qty if qty < 1e-12: qty = 0.0 mark_price = float(last_price if last_price is not None else last_trade_price or 0) holding_value = qty * mark_price final_asset = cash + holding_value pnl_krw = final_asset - initial_cash pnl_pct = pnl_krw / initial_cash * 100.0 if initial_cash else 0.0 return { "initial_cash_krw": round(initial_cash, 0), "final_asset_krw": round(final_asset, 0), "pnl_krw": round(pnl_krw, 0), "pnl_pct": round(pnl_pct, 2), "total_fees_krw": round(total_fees, 0), "cash_krw": round(cash, 0), "holding_qty": round(qty, 6), "holding_value_krw": round(holding_value, 0), "mark_price": round(mark_price, 2), "fee_rate": fee_rate, "order_krw": round(order, 0), "sizing_mode": sizing_mode, "trade_count": len(trades), } def simulate_fixed_order_portfolio_steps( trades: list[dict[str, Any]], order_krw: float = LIVE_ORDER_KRW, initial_cash: float = GT_INITIAL_CASH_KRW, fee_rate: float = TRADING_FEE_RATE, sizing_mode: str = "fixed", ) -> list[dict[str, Any]]: """ 체결마다 현금·보유·총평가 스냅샷 (GT 테이블용). Args: trades: 시간순 trade dict. order_krw: 1회 체결 원화. initial_cash: 시작 현금. fee_rate: 수수료율. sizing_mode: fixed | amount_krw. Returns: step dict 리스트. """ cash = float(initial_cash) qty = 0.0 order = float(order_krw) steps: list[dict[str, Any]] = [] for t in sorted(trades, key=lambda x: x["dt"]): action = t["action"] price = float(t["price"]) if price <= 0: continue if action == "buy": planned = _planned_order_krw(t, order, sizing_mode) amount = min(planned, max(cash / (1.0 + fee_rate), 0.0)) if amount <= 0: continue fee = amount * fee_rate cash -= amount + fee qty += amount / price elif action == "sell" and qty > 0: planned = _planned_order_krw(t, order, sizing_mode) if planned >= qty * price * 0.999: sell_qty = qty else: sell_qty = min(qty, planned / price) if sell_qty <= 0: continue gross = sell_qty * price fee = gross * fee_rate cash += gross - fee qty -= sell_qty if qty < 1e-12: qty = 0.0 steps.append( { "dt": t["dt"], "action": action, "price": price, "rule_id": t.get("rule_id", ""), "forward_ret_pct": t.get("forward_ret_pct"), "amount_krw": t.get("amount_krw"), "cash_krw": round(cash, 0), "holding_qty": round(qty, 4), "total_asset_krw": round(cash + qty * price, 0), } ) return steps