인과적 GT 신호·복리 배분 시뮬을 도입하고 운영 정합성을 맞춘다.
미래 데이터를 쓰지 않는 causal 신호/tier와 전기간 복리 포트폴리오 비교로 GT 대비 sim_sized 검증 경로를 정리하고, 일한도·매수 상한·live_buy 스케일을 제거한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
규칙 발화 기반 고정 금액 체결 포트폴리오 시뮬 (GT HTML 카드·테이블용).
|
||||
규칙 발화 기반 GT 모델 복리 포트폴리오 시뮬.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -10,14 +10,13 @@ import pandas as pd
|
||||
|
||||
from config import (
|
||||
GT_INITIAL_CASH_KRW,
|
||||
LIVE_DAILY_KRW_MAX,
|
||||
LIVE_MAX_TRADES_PER_DAY,
|
||||
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_dynamic_buy_amounts,
|
||||
load_sizing_context_from_gt,
|
||||
attach_gt_model_amounts,
|
||||
)
|
||||
|
||||
|
||||
@@ -43,99 +42,95 @@ def _planned_order_krw(
|
||||
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 (dt, side, close, rule_id …).
|
||||
fires: fire_outcomes.
|
||||
use_dynamic_sizing: 미사용 (하위 호환).
|
||||
|
||||
Returns:
|
||||
체결된 발화 DataFrame.
|
||||
정렬된 발화 DataFrame.
|
||||
"""
|
||||
if fires.empty:
|
||||
return fires
|
||||
gt_trades, large_legs, approved = load_sizing_context_from_gt()
|
||||
df = fires.sort_values("dt").copy()
|
||||
df["ts"] = pd.to_datetime(df["dt"])
|
||||
df["day"] = df["ts"].dt.date.astype(str)
|
||||
cash = float(GT_INITIAL_CASH_KRW)
|
||||
qty = 0.0
|
||||
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
|
||||
side = row["side"]
|
||||
price = float(row["close"])
|
||||
if side == "buy" and use_dynamic_sizing:
|
||||
from deepcoin.matching.position_sizing import (
|
||||
compute_buy_amount_krw,
|
||||
live_buy_asset_pct_scale,
|
||||
)
|
||||
|
||||
scale = live_buy_asset_pct_scale(
|
||||
str(row["rule_id"]),
|
||||
str(row["dt"]),
|
||||
gt_trades,
|
||||
approved_rules=approved,
|
||||
large_legs=large_legs,
|
||||
)
|
||||
planned = compute_buy_amount_krw(
|
||||
cash,
|
||||
qty,
|
||||
price,
|
||||
1.0,
|
||||
1.0,
|
||||
asset_pct_scale=scale,
|
||||
)
|
||||
else:
|
||||
planned = float(LIVE_ORDER_KRW)
|
||||
if side == "buy":
|
||||
if spent + planned > LIVE_DAILY_KRW_MAX:
|
||||
break
|
||||
if planned <= 0:
|
||||
continue
|
||||
fee = planned * TRADING_FEE_RATE
|
||||
cash -= planned + fee
|
||||
qty += planned / price if price > 0 else 0.0
|
||||
spent += planned
|
||||
elif side == "sell" and qty > 0:
|
||||
gross = qty * price
|
||||
cash += gross * (1.0 - TRADING_FEE_RATE)
|
||||
qty = 0.0
|
||||
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)
|
||||
_ = 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]]:
|
||||
"""
|
||||
발화 DataFrame을 포트폴리오 시뮬용 trade dict 리스트로 변환.
|
||||
발화 → GT 모델 amount_krw가 채워진 trade dict (복리 배분).
|
||||
|
||||
Args:
|
||||
fires: 체결 대상 발화.
|
||||
apply_dynamic_sizing: True면 GT tier 복리 배분.
|
||||
initial_cash: 시작 현금 (누적 복리).
|
||||
fee_rate: 수수료율.
|
||||
|
||||
Returns:
|
||||
dt, action, price 키를 가진 dict 리스트.
|
||||
dt, action, price, amount_krw 키 dict 리스트.
|
||||
"""
|
||||
if fires.empty:
|
||||
return []
|
||||
rows: list[dict[str, Any]] = []
|
||||
for _, r in fires.sort_values("dt").iterrows():
|
||||
for _, r in sort_fires_chronological(fires).iterrows():
|
||||
rows.append(
|
||||
{
|
||||
"dt": str(r["dt"]),
|
||||
@@ -145,13 +140,11 @@ def fires_to_trade_list(
|
||||
"forward_ret_pct": float(r.get("forward_ret_pct", 0)),
|
||||
}
|
||||
)
|
||||
if apply_dynamic_sizing and rows:
|
||||
gt_trades, large_legs, approved = load_sizing_context_from_gt()
|
||||
attach_dynamic_buy_amounts(
|
||||
if apply_dynamic_sizing:
|
||||
attach_gt_model_amounts(
|
||||
rows,
|
||||
gt_trades=gt_trades,
|
||||
approved_rules=approved,
|
||||
large_legs=large_legs,
|
||||
initial_cash=initial_cash,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
return rows
|
||||
|
||||
@@ -164,7 +157,7 @@ def simulate_sized_portfolio(
|
||||
fallback_order_krw: float = LIVE_ORDER_KRW,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
trade.amount_krw(총자산 비율 배분) 기준 포트폴리오 시뮬.
|
||||
trade.amount_krw(GT 모델·복리 배분) 기준 포트폴리오 시뮬 + MDD.
|
||||
|
||||
Args:
|
||||
trades: 시간순 trade dict (amount_krw 권장).
|
||||
@@ -174,16 +167,25 @@ def simulate_sized_portfolio(
|
||||
fallback_order_krw: amount_krw 없을 때 1회 금액.
|
||||
|
||||
Returns:
|
||||
simulate_truth_portfolio와 동일 키 구조.
|
||||
simulate_truth_portfolio와 동일 키 + max_drawdown_pct.
|
||||
"""
|
||||
return simulate_fixed_order_portfolio(
|
||||
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,
|
||||
order_krw=fallback_order_krw,
|
||||
initial_cash=initial_cash,
|
||||
fee_rate=fee_rate,
|
||||
last_price=last_price,
|
||||
sizing_mode="amount_krw",
|
||||
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(
|
||||
@@ -203,7 +205,7 @@ def simulate_fixed_order_portfolio(
|
||||
initial_cash: 시작 현금.
|
||||
fee_rate: 수수료율.
|
||||
last_price: 미청산 평가 종가.
|
||||
sizing_mode: 'fixed' | 'amount_krw' (없으면 order_krw).
|
||||
sizing_mode: 'fixed' | 'amount_krw'.
|
||||
|
||||
Returns:
|
||||
simulate_truth_portfolio와 동일 키 구조.
|
||||
|
||||
Reference in New Issue
Block a user