GT 총자산 비율 매수·leg 티어 배분과 시뮬/실거래 포지션 사이징을 통합한다.

타점·비중을 gt_model로 일반화하고, amount_krw 시각순 배분·EV/WF·상위 leg 대형 매수를
position_sizing과 시뮬 HTML(고정 ₩/회 비교)에 반영한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 16:11:49 +09:00
parent 2cb67c42b3
commit 5842cc9fa3
14 changed files with 2073 additions and 182 deletions

View File

@@ -12,6 +12,7 @@ import numpy as np
import pandas as pd
from config import (
GT_INITIAL_CASH_KRW,
LIVE_DAILY_KRW_MAX,
LIVE_MAX_TRADES_PER_DAY,
LIVE_ORDER_KRW,
@@ -28,7 +29,19 @@ from config import (
SIM_WALK_FORWARD_MIN_MONTHS,
TRADING_FEE_RATE,
)
from deepcoin.ground_truth.ground_truth import (
load_ground_truth,
order_trades_chronological,
simulate_truth_portfolio,
)
from deepcoin.matching.portfolio_sim import (
fires_to_trade_list,
select_capped_fires,
simulate_fixed_order_portfolio,
simulate_sized_portfolio,
)
from deepcoin.matching.select_rules import _rule_metrics, _split_train_valid_holdout
from deepcoin.paths import resolve_ground_truth_file
from deepcoin.paths import (
ANALYSIS_GT_CALIBRATION_JSON,
MATCHING_FIRE_OUTCOMES,
@@ -109,12 +122,19 @@ def walk_forward_summary(wf_rows: list[dict[str, Any]]) -> dict[str, Any]:
return out
def simulate_live_order_cap(outcomes: pd.DataFrame) -> dict[str, Any]:
def simulate_live_order_cap(
outcomes: pd.DataFrame,
*,
rule_ids: set[str] | None = None,
holdout_only: bool = True,
) -> dict[str, Any]:
"""
1회·일 한도·슬리피지 가정으로 체결 가능한 발화만 집계.
Args:
outcomes: fire_outcomes.
outcomes: fire_outcomes (split 컬럼 있으면 holdout 필터 가능).
rule_ids: None이면 전 규칙, 지정 시 해당 rule만.
holdout_only: True면 split==holdout 만.
Returns:
규칙별·전체 요약.
@@ -122,12 +142,27 @@ def simulate_live_order_cap(outcomes: pd.DataFrame) -> dict[str, Any]:
if outcomes.empty:
return {"rules": {}, "note": "발화 없음"}
df = outcomes.sort_values("dt").copy()
df = outcomes
if holdout_only and "split" in df.columns:
df = df[df["split"] == "holdout"]
if rule_ids is not None:
df = df[df["rule_id"].isin(rule_ids)]
df = df.sort_values("dt").copy()
df["ts"] = pd.to_datetime(df["dt"])
df["day"] = df["ts"].dt.date.astype(str)
slip = LIVE_SLIPPAGE_PCT
taken_rows: list[pd.DataFrame] = []
from deepcoin.matching.position_sizing import (
compute_buy_amount_krw,
live_buy_asset_pct_scale,
load_sizing_context_from_gt,
)
gt_trades, large_legs, approved = load_sizing_context_from_gt()
cash = float(GT_INITIAL_CASH_KRW)
qty = 0.0
for day, day_grp in df.groupby("day", sort=True):
spent = 0.0
n_trades = 0
@@ -135,9 +170,34 @@ def simulate_live_order_cap(outcomes: pd.DataFrame) -> dict[str, 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
side = row["side"]
price = float(row["close"])
if side == "buy":
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 planned <= 0:
continue
if spent + planned > LIVE_DAILY_KRW_MAX:
break
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
taken_idx.append(idx)
if taken_idx:
@@ -164,6 +224,7 @@ def simulate_live_order_cap(outcomes: pd.DataFrame) -> dict[str, Any]:
"order_krw": LIVE_ORDER_KRW,
"daily_krw_max": LIVE_DAILY_KRW_MAX,
"slippage_pct": slip,
"sizing": "total_asset_pct_ev_wf_large_leg",
},
"taken_count": int(len(taken)),
"total_count": int(len(df)),
@@ -267,9 +328,35 @@ def build_simulation_report(
sub.assign(forward_ret_pct=adj)
)
live_cap = simulate_live_order_cap(outcomes)
monitor_ids = {r["rule_id"] for r in matched.get("monitor_rules", [])}
live_cap = simulate_live_order_cap(
outcomes, rule_ids=monitor_ids, holdout_only=True
)
go = evaluate_go_no_go(matched, wf_sum, fee_stress, live_cap)
portfolio_compare: dict[str, Any] = {}
gt_data = load_ground_truth(resolve_ground_truth_file()) or {}
gt_trades = gt_data.get("trades") or []
mark = (gt_data.get("summary") or {}).get("mark_price")
if gt_trades:
portfolio_compare["ground_truth_chrono"] = simulate_truth_portfolio(
order_trades_chronological(gt_trades),
last_price=float(mark) if mark else None,
)
holdout = outcomes[
outcomes["rule_id"].isin(monitor_ids) & (outcomes["split"] == "holdout")
]
capped = select_capped_fires(holdout)
if not capped.empty:
portfolio_compare["sim_sized"] = simulate_sized_portfolio(
fires_to_trade_list(capped, apply_dynamic_sizing=True),
last_price=float(mark) if mark else None,
)
portfolio_compare["sim_fixed_order"] = simulate_fixed_order_portfolio(
fires_to_trade_list(capped, apply_dynamic_sizing=False),
last_price=float(mark) if mark else None,
)
gt_portfolio: dict[str, Any] = {}
if ANALYSIS_GT_CALIBRATION_JSON.is_file():
cal = json.loads(ANALYSIS_GT_CALIBRATION_JSON.read_text(encoding="utf-8"))
@@ -301,6 +388,8 @@ def build_simulation_report(
"fee_stress_by_rule": fee_stress,
"live_order_cap_sim": live_cap,
"go_no_go": go,
"portfolio_compare": portfolio_compare,
"gt_model": gt_data.get("model"),
"monitor_rules": matched.get("monitor_rules", []),
"gt_portfolio_calibration": gt_portfolio,
"criteria": {