GT MTF 프로필·캘리브레이션과 04 매칭/시뮬/실거래 파이프라인을 추가한다.
3분~일봉 GT 타점 분석(03c), leg 체결 순서 수정, 총자산 90% 검증 루프, walk-forward Go/No-Go 시뮬, monitor·live_trader 및 reference 문서를 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
215
deepcoin/matching/portfolio_sim.py
Normal file
215
deepcoin/matching/portfolio_sim.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
규칙 발화 기반 고정 금액 체결 포트폴리오 시뮬 (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
|
||||
Reference in New Issue
Block a user