""" 규칙 발화 기반 고정 금액 체결 포트폴리오 시뮬 (GT HTML 카드·테이블용). """ from __future__ import annotations from typing import Any import pandas as pd from config import ( GT_INITIAL_CASH_KRW, LIVE_DAILY_KRW_MAX, LIVE_MAX_TRADES_PER_DAY, LIVE_ORDER_KRW, TRADING_FEE_RATE, ) def select_capped_fires(fires: pd.DataFrame) -> pd.DataFrame: """ 일한도·회수 제한으로 체결 가능한 발화만 남깁니다. Args: fires: fire_outcomes (dt, side, close, rule_id …). Returns: 체결된 발화 DataFrame. """ if fires.empty: return fires df = fires.sort_values("dt").copy() df["ts"] = pd.to_datetime(df["dt"]) df["day"] = df["ts"].dt.date.astype(str) taken: list[pd.DataFrame] = [] for _, day_grp in df.groupby("day", sort=True): spent = 0.0 n_trades = 0 idxs: list[Any] = [] for idx, _row in day_grp.iterrows(): if n_trades >= LIVE_MAX_TRADES_PER_DAY: break if spent + LIVE_ORDER_KRW > LIVE_DAILY_KRW_MAX: break spent += LIVE_ORDER_KRW n_trades += 1 idxs.append(idx) if idxs: taken.append(day_grp.loc[idxs]) if not taken: return df.iloc[0:0] return pd.concat(taken, ignore_index=True) def fires_to_trade_list(fires: pd.DataFrame) -> list[dict[str, Any]]: """ 발화 DataFrame을 포트폴리오 시뮬용 trade dict 리스트로 변환. Args: fires: 체결 대상 발화. Returns: dt, action, price 키를 가진 dict 리스트. """ rows: list[dict[str, Any]] = [] for _, r in fires.sort_values("dt").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)), } ) return rows 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, ) -> dict[str, Any]: """ 매 체결마다 고정 원화 금액으로 매수·매도한 뒤 총평가·수익률을 계산합니다. Args: trades: 시간순 {dt, action, price}. order_krw: 1회 매수·매도 금액(원). initial_cash: 시작 현금. fee_rate: 수수료율. last_price: 미청산 평가 종가. 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": amount = min(order, 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: sell_qty = min(qty, order / 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), "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, ) -> list[dict[str, Any]]: """ 체결마다 현금·보유·총평가 스냅샷 (GT 테이블용). Args: trades: 시간순 trade dict. order_krw: 1회 체결 원화. initial_cash: 시작 현금. fee_rate: 수수료율. 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": amount = min(order, 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: sell_qty = min(qty, order / 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"), "cash_krw": round(cash, 0), "holding_qty": round(qty, 4), "total_asset_krw": round(cash + qty * price, 0), } ) return steps