GT 총자산 비율 매수·leg 티어 배분과 시뮬/실거래 포지션 사이징을 통합한다.
타점·비중을 gt_model로 일반화하고, amount_krw 시각순 배분·EV/WF·상위 leg 대형 매수를 position_sizing과 시뮬 HTML(고정 ₩/회 비교)에 반영한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -28,7 +28,12 @@ from config import (
|
||||
GT_BUY_BB_MAX,
|
||||
GT_BUY_MIN_BARS,
|
||||
GT_BUY_MIN_SWING_PCT,
|
||||
GT_BUY_PCT_LARGE_LEG,
|
||||
GT_BUY_PCT_SMALL_LEG,
|
||||
GT_INITIAL_CASH_KRW,
|
||||
GT_LARGE_LEG_TOP_PCT,
|
||||
GT_MIN_ORDER_KRW,
|
||||
GT_MAX_BUY_ORDER_KRW,
|
||||
GT_MAX_BUYS_PER_LEG,
|
||||
GT_MAX_ROUND_TRIPS,
|
||||
TRADING_FEE_RATE,
|
||||
@@ -68,6 +73,7 @@ class TradePoint:
|
||||
price: float
|
||||
memo: str
|
||||
weight: float = 1.0
|
||||
amount_krw: float | None = None
|
||||
leg_id: int = 0
|
||||
bb_pos: float | None = None
|
||||
rsi: float | None = None
|
||||
@@ -844,6 +850,12 @@ def generate_ground_truth(
|
||||
)
|
||||
|
||||
trade_dicts = order_trades_leg_block(trades)
|
||||
trade_dicts, alloc_stats = allocate_gt_order_amounts(
|
||||
trade_dicts,
|
||||
initial_cash=GT_INITIAL_CASH_KRW,
|
||||
min_order_krw=GT_MIN_ORDER_KRW,
|
||||
fee_rate=TRADING_FEE_RATE,
|
||||
)
|
||||
last_close = float(df["Close"].iloc[-1])
|
||||
pnl = simulate_truth_portfolio(
|
||||
trade_dicts,
|
||||
@@ -859,8 +871,13 @@ def generate_ground_truth(
|
||||
)
|
||||
_validate_leg_portfolio(trade_dicts, last_close)
|
||||
|
||||
from deepcoin.ground_truth.gt_model import default_model, model_to_dict
|
||||
|
||||
gt_model = model_to_dict(default_model())
|
||||
|
||||
return {
|
||||
"name": "ground_truth_split_buy_peak_sell",
|
||||
"model": gt_model,
|
||||
"method": method,
|
||||
"symbol": SYMBOL,
|
||||
"interval_min": ENTRY_INTERVAL,
|
||||
@@ -893,12 +910,23 @@ def generate_ground_truth(
|
||||
"unrealized_pnl_krw": round(
|
||||
float(pnl.get("pnl_krw", 0)) - float(pnl_realized.get("pnl_krw", 0)), 0
|
||||
),
|
||||
"execution_order": "leg_block",
|
||||
"execution_order": (
|
||||
"chronological"
|
||||
if any(float(t.get("amount_krw") or 0) > 0 for t in trade_dicts)
|
||||
else "leg_block"
|
||||
),
|
||||
"order_amount_min_krw": GT_MIN_ORDER_KRW,
|
||||
"order_amount_max_buy_krw": GT_MAX_BUY_ORDER_KRW,
|
||||
"buy_pct_large_leg": GT_BUY_PCT_LARGE_LEG,
|
||||
"buy_pct_small_leg": GT_BUY_PCT_SMALL_LEG,
|
||||
"large_leg_top_pct": GT_LARGE_LEG_TOP_PCT,
|
||||
**alloc_stats,
|
||||
},
|
||||
"note": (
|
||||
"저점 분할 매수(비중=삼각형), 고점 1~2회 매도. "
|
||||
"체결 순서=leg별 매수→매도(시각순 아님). 기간말 leg는 종가 청산. "
|
||||
"summary.pnl_pct는 미청산 포함 종가 평가, realized_pnl_pct는 체결만 반영."
|
||||
"매수=총자산×최적비중×티어(상위 leg 대형·그 외 소형), "
|
||||
f"현금 한도·최소 ₩{GT_MIN_ORDER_KRW:,}. "
|
||||
"체결 순서=chronological. summary.pnl_pct는 미청산 포함 종가 평가."
|
||||
),
|
||||
"trades": trade_dicts,
|
||||
}
|
||||
@@ -921,18 +949,6 @@ def _validate_leg_portfolio(
|
||||
steps = simulate_truth_portfolio_steps(trade_dicts)
|
||||
if not steps:
|
||||
return
|
||||
leg_ids = sorted({int(s["leg_id"]) for s in steps})
|
||||
for lid in leg_ids:
|
||||
leg_steps = [s for s in steps if int(s["leg_id"]) == lid]
|
||||
sells = [s for s in leg_steps if s["action"] == "sell"]
|
||||
if not sells:
|
||||
continue
|
||||
last_sell = sells[-1]
|
||||
if float(last_sell["holding_qty"]) > 1e-4:
|
||||
raise ValueError(
|
||||
f"leg#{lid} 마지막 매도 후 보유 잔존 qty={last_sell['holding_qty']} "
|
||||
"(leg 블록 체결·매도 비중 합 검토 필요)"
|
||||
)
|
||||
final = steps[-1]
|
||||
if float(final["holding_qty"]) > 1e-2:
|
||||
raise ValueError(
|
||||
@@ -943,6 +959,203 @@ def _validate_leg_portfolio(
|
||||
raise ValueError("종가 평가 후에도 미청산 보유가 남음")
|
||||
|
||||
|
||||
def allocate_gt_order_amounts(
|
||||
trades: list[dict[str, Any]],
|
||||
initial_cash: float = GT_INITIAL_CASH_KRW,
|
||||
min_order_krw: float = GT_MIN_ORDER_KRW,
|
||||
max_buy_krw: float = GT_MAX_BUY_ORDER_KRW,
|
||||
fee_rate: float = TRADING_FEE_RATE,
|
||||
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
||||
"""
|
||||
GT 각 타점에 amount_krw를 시각순·총자산·비중(최적 매수율)으로 배분합니다.
|
||||
|
||||
매수: 총보유자산 × (leg 비중 share × 티어 스케일), 상한=가용 현금.
|
||||
leg 상위 GT_LARGE_LEG_TOP_PCT는 GT_BUY_PCT_LARGE_LEG, 그 외는 GT_BUY_PCT_SMALL_LEG.
|
||||
매도 후 현금 증가분은 다음 매수부터 자동 반영(시각순 복리).
|
||||
|
||||
Args:
|
||||
trades: trade dict 리스트(시각순 정렬 전).
|
||||
initial_cash: 초기 현금.
|
||||
min_order_krw: 매수·매도 최소 원화 금액.
|
||||
max_buy_krw: 매수 1회 상한(가용 현금·비중 배분 후 캡).
|
||||
fee_rate: 수수료율.
|
||||
|
||||
Returns:
|
||||
(동일 dict 참조, amount_krw 채움), alloc_stats 요약.
|
||||
"""
|
||||
from deepcoin.matching.position_sizing import (
|
||||
compute_buy_amount_krw,
|
||||
leg_asset_pct_scale,
|
||||
top_leg_ids_by_forward_return,
|
||||
)
|
||||
|
||||
chron = sorted(trades, key=lambda x: x["dt"])
|
||||
large_legs = top_leg_ids_by_forward_return(chron)
|
||||
leg_buy_idxs: dict[int, list[int]] = {}
|
||||
leg_sell_idxs: dict[int, list[int]] = {}
|
||||
for i, t in enumerate(chron):
|
||||
lid = int(t.get("leg_id", 0))
|
||||
if t["action"] == "buy":
|
||||
leg_buy_idxs.setdefault(lid, []).append(i)
|
||||
elif t["action"] == "sell":
|
||||
leg_sell_idxs.setdefault(lid, []).append(i)
|
||||
|
||||
cash = float(initial_cash)
|
||||
qty = 0.0
|
||||
qty_by_leg: dict[int, float] = {}
|
||||
sell_leg: int | None = None
|
||||
sell_base_qty = 0.0
|
||||
buy_executed = 0
|
||||
buy_skipped = 0
|
||||
sell_executed = 0
|
||||
sell_skipped = 0
|
||||
buy_amounts: list[float] = []
|
||||
|
||||
for i, t in enumerate(chron):
|
||||
price = float(t["price"])
|
||||
if price <= 0:
|
||||
continue
|
||||
leg_id = int(t.get("leg_id", 0))
|
||||
weight = float(t.get("weight", 1.0))
|
||||
|
||||
if t["action"] == "buy":
|
||||
rem = [j for j in leg_buy_idxs.get(leg_id, []) if j >= i]
|
||||
w_sum = sum(float(chron[j].get("weight", 1.0)) for j in rem)
|
||||
w_share = (
|
||||
weight / w_sum if w_sum > 0 else 1.0 / max(len(rem), 1)
|
||||
)
|
||||
scale = leg_asset_pct_scale(leg_id, large_legs)
|
||||
amount = compute_buy_amount_krw(
|
||||
cash,
|
||||
qty,
|
||||
price,
|
||||
weight,
|
||||
w_sum,
|
||||
asset_pct_scale=scale,
|
||||
min_order_krw=min_order_krw,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
if amount <= 0:
|
||||
t["amount_krw"] = 0
|
||||
buy_skipped += 1
|
||||
continue
|
||||
t["amount_krw"] = amount
|
||||
fee = amount * fee_rate
|
||||
cash -= amount + fee
|
||||
bought_qty = amount / price
|
||||
qty += bought_qty
|
||||
qty_by_leg[leg_id] = qty_by_leg.get(leg_id, 0.0) + bought_qty
|
||||
buy_executed += 1
|
||||
buy_amounts.append(amount)
|
||||
sell_leg = None
|
||||
|
||||
elif t["action"] == "sell":
|
||||
leg_qty = qty_by_leg.get(leg_id, 0.0)
|
||||
if leg_qty <= 1e-12:
|
||||
sell_skipped += 1
|
||||
continue
|
||||
if sell_leg != leg_id:
|
||||
sell_leg = leg_id
|
||||
sell_base_qty = leg_qty
|
||||
rem_sells = [j for j in leg_sell_idxs.get(leg_id, []) if j >= i]
|
||||
is_last_leg_sell = bool(rem_sells) and i == rem_sells[-1]
|
||||
if is_last_leg_sell:
|
||||
sell_qty = leg_qty
|
||||
gross = sell_qty * price
|
||||
else:
|
||||
gross = sell_base_qty * weight * price
|
||||
if gross >= min_order_krw:
|
||||
gross = max(min_order_krw, gross)
|
||||
gross = min(gross, leg_qty * price)
|
||||
if gross <= 0:
|
||||
sell_skipped += 1
|
||||
continue
|
||||
if not is_last_leg_sell:
|
||||
sell_qty = gross / price
|
||||
else:
|
||||
sell_qty = leg_qty
|
||||
t["amount_krw"] = round(gross, 0)
|
||||
fee = gross * fee_rate
|
||||
cash += gross - fee
|
||||
leg_qty -= sell_qty
|
||||
qty_by_leg[leg_id] = max(leg_qty, 0.0)
|
||||
qty = max(qty - sell_qty, 0.0)
|
||||
if qty < 1e-12:
|
||||
qty = 0.0
|
||||
sell_executed += 1
|
||||
|
||||
stats: dict[str, Any] = {
|
||||
"buy_executed": buy_executed,
|
||||
"buy_skipped": buy_skipped,
|
||||
"sell_executed": sell_executed,
|
||||
"sell_skipped": sell_skipped,
|
||||
"buy_total_krw": round(sum(buy_amounts), 0),
|
||||
"large_leg_count": len(large_legs),
|
||||
"large_leg_top_pct": GT_LARGE_LEG_TOP_PCT,
|
||||
}
|
||||
if buy_amounts:
|
||||
stats["buy_amount_avg_krw"] = round(sum(buy_amounts) / len(buy_amounts), 0)
|
||||
stats["buy_amount_min_krw"] = round(min(buy_amounts), 0)
|
||||
stats["buy_amount_max_krw"] = round(max(buy_amounts), 0)
|
||||
return trades, stats
|
||||
|
||||
|
||||
def _resolve_sell_qty(
|
||||
t: dict[str, Any],
|
||||
qty: float,
|
||||
price: float,
|
||||
sell_base_qty: float,
|
||||
weight: float,
|
||||
) -> float:
|
||||
"""
|
||||
매도 수량: amount_krw가 보유 전량에 가깝으면 전량, 아니면 weight 비중.
|
||||
|
||||
Args:
|
||||
t: trade dict.
|
||||
qty: 현재 보유 수량.
|
||||
price: 체결가.
|
||||
sell_base_qty: leg 첫 매도 시점 보유량.
|
||||
weight: 매도 비중.
|
||||
|
||||
Returns:
|
||||
매도 수량.
|
||||
"""
|
||||
if qty <= 0 or price <= 0:
|
||||
return 0.0
|
||||
ak = t.get("amount_krw")
|
||||
if ak is not None and float(ak) > 0:
|
||||
gross_cap = float(ak)
|
||||
if gross_cap >= qty * price * 0.999:
|
||||
return qty
|
||||
return min(qty, gross_cap / price)
|
||||
return min(sell_base_qty * weight, qty)
|
||||
|
||||
|
||||
def _trade_buy_amount(
|
||||
t: dict[str, Any],
|
||||
cash: float,
|
||||
leg_budget: float,
|
||||
current_leg: int | None,
|
||||
leg_id: int,
|
||||
fee_rate: float,
|
||||
) -> tuple[float, float, int | None]:
|
||||
"""
|
||||
매수 체결 원화: amount_krw 우선, 없으면 leg_budget*weight.
|
||||
|
||||
Returns:
|
||||
(amount, new_leg_budget, new_current_leg).
|
||||
"""
|
||||
weight = float(t.get("weight", 1.0))
|
||||
if t.get("amount_krw") is not None and float(t["amount_krw"]) > 0:
|
||||
amount = min(float(t["amount_krw"]), max(cash / (1.0 + fee_rate), 0.0))
|
||||
return amount, leg_budget, current_leg
|
||||
if leg_id != current_leg:
|
||||
current_leg = leg_id
|
||||
leg_budget = cash
|
||||
amount = leg_budget * weight
|
||||
return amount, leg_budget, current_leg
|
||||
|
||||
|
||||
def order_trades_leg_block(
|
||||
trades: list[TradePoint] | list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -996,8 +1209,12 @@ def _truth_simulation_rows(
|
||||
Returns:
|
||||
dict 행 리스트.
|
||||
"""
|
||||
if chronological:
|
||||
return order_trades_chronological(trades)
|
||||
rows = [t if isinstance(t, dict) else asdict(t) for t in trades]
|
||||
use_chrono = chronological or any(
|
||||
float(r.get("amount_krw") or 0) > 0 for r in rows
|
||||
)
|
||||
if use_chrono:
|
||||
return sorted(rows, key=lambda x: x["dt"])
|
||||
return order_trades_leg_block(trades)
|
||||
|
||||
|
||||
@@ -1037,7 +1254,9 @@ def simulate_truth_portfolio_steps(
|
||||
current_leg = leg_id
|
||||
leg_budget = cash
|
||||
sell_leg = None
|
||||
amount = leg_budget * weight
|
||||
amount, leg_budget, current_leg = _trade_buy_amount(
|
||||
t, cash, leg_budget, current_leg, leg_id, fee_rate
|
||||
)
|
||||
if amount <= 0:
|
||||
continue
|
||||
fee = amount * fee_rate
|
||||
@@ -1054,7 +1273,7 @@ def simulate_truth_portfolio_steps(
|
||||
if leg_id != sell_leg:
|
||||
sell_leg = leg_id
|
||||
sell_base_qty = qty
|
||||
sell_qty = min(sell_base_qty * weight, qty)
|
||||
sell_qty = _resolve_sell_qty(t, qty, price, sell_base_qty, weight)
|
||||
if sell_qty <= 0:
|
||||
continue
|
||||
gross = sell_qty * price
|
||||
@@ -1071,6 +1290,7 @@ def simulate_truth_portfolio_steps(
|
||||
"action": action,
|
||||
"price": price,
|
||||
"weight": weight,
|
||||
"amount_krw": t.get("amount_krw"),
|
||||
"leg_id": leg_id,
|
||||
"cash_krw": round(cash, 0),
|
||||
"holding_qty": round(qty, 4),
|
||||
@@ -1130,7 +1350,9 @@ def simulate_truth_portfolio(
|
||||
current_leg = leg_id
|
||||
leg_budget = cash
|
||||
sell_leg = None
|
||||
amount = leg_budget * weight
|
||||
amount, leg_budget, current_leg = _trade_buy_amount(
|
||||
t, cash, leg_budget, current_leg, leg_id, fee_rate
|
||||
)
|
||||
if amount <= 0:
|
||||
continue
|
||||
fee = amount * fee_rate
|
||||
@@ -1148,7 +1370,7 @@ def simulate_truth_portfolio(
|
||||
if leg_id != sell_leg:
|
||||
sell_leg = leg_id
|
||||
sell_base_qty = qty
|
||||
sell_qty = min(sell_base_qty * weight, qty)
|
||||
sell_qty = _resolve_sell_qty(t, qty, price, sell_base_qty, weight)
|
||||
if sell_qty <= 0:
|
||||
continue
|
||||
gross = sell_qty * price
|
||||
|
||||
228
deepcoin/ground_truth/gt_model.py
Normal file
228
deepcoin/ground_truth/gt_model.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Ground Truth 타점·비중·자본 배분 모델 (일반화 명세).
|
||||
|
||||
타점 생성(ground_truth.py)과 자본 배분(position_sizing.py)의 공통 언어.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from config import (
|
||||
GT_BUY_BB_MAX,
|
||||
GT_BUY_MIN_BARS,
|
||||
GT_BUY_MIN_SWING_PCT,
|
||||
GT_BUY_PCT_LARGE_LEG,
|
||||
GT_BUY_PCT_SMALL_LEG,
|
||||
GT_LARGE_LEG_TOP_PCT,
|
||||
GT_MAX_BUYS_PER_LEG,
|
||||
GT_MAX_SELLS_PER_LEG,
|
||||
GT_MIN_ORDER_KRW,
|
||||
GT_SELL_SPLIT_GAP_PCT,
|
||||
GT_SELECTION_MODE,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GtEntrySpec:
|
||||
"""
|
||||
매수 타점(leg 내 분할) 규칙.
|
||||
|
||||
Attributes:
|
||||
pivot_kind: ZigZag 저점(trough).
|
||||
price_field: 체결가 = 봉 Low.
|
||||
weight_rule: 저가일수록 큰 비중 (1/price 정규화).
|
||||
max_per_leg: leg당 최대 매수 횟수.
|
||||
min_bars_gap: 분할 매수 최소 봉 간격.
|
||||
"""
|
||||
|
||||
pivot_kind: str = "trough"
|
||||
price_field: str = "Low"
|
||||
weight_rule: str = "inverse_price_normalized"
|
||||
max_per_leg: int = GT_MAX_BUYS_PER_LEG
|
||||
min_bars_gap: int = GT_BUY_MIN_BARS
|
||||
bb_filter: str = f"bb_pos <= {GT_BUY_BB_MAX}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GtExitSpec:
|
||||
"""
|
||||
매도 타점(leg 내 분할) 규칙.
|
||||
|
||||
Attributes:
|
||||
pivot_kind: major swing 고점(peak).
|
||||
price_field: 체결가 = 봉 High.
|
||||
split_weights: 2회 분할 시 (65%, 35%).
|
||||
split_gap_pct: 2차 고점 인정 최소 괴리(%).
|
||||
"""
|
||||
|
||||
pivot_kind: str = "peak"
|
||||
price_field: str = "High"
|
||||
split_weights: tuple[float, float] = (0.65, 0.35)
|
||||
split_gap_pct: float = GT_SELL_SPLIT_GAP_PCT
|
||||
max_per_leg: int = GT_MAX_SELLS_PER_LEG
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GtCapitalSpec:
|
||||
"""
|
||||
체결 원화(amount_krw) 배분 규칙.
|
||||
|
||||
Attributes:
|
||||
buy_formula: 총자산 × 최적매수율, 상한=가용현금.
|
||||
optimal_buy_rate: leg 내 남은 weight 비중.
|
||||
large_leg_top_pct: 수익률 상위 leg 비율.
|
||||
pct_large: 상위 leg 총자산 배분 스케일.
|
||||
pct_small: 그 외 leg 스케일.
|
||||
min_order_krw: 최소 체결 원화.
|
||||
"""
|
||||
|
||||
buy_formula: str = "min(total_asset * w_share * tier_scale, cash/(1+fee))"
|
||||
optimal_buy_rate: str = "weight / sum(remaining_buy_weights_in_leg)"
|
||||
large_leg_top_pct: float = GT_LARGE_LEG_TOP_PCT
|
||||
pct_large: float = GT_BUY_PCT_LARGE_LEG
|
||||
pct_small: float = GT_BUY_PCT_SMALL_LEG
|
||||
min_order_krw: float = float(GT_MIN_ORDER_KRW)
|
||||
sell_formula: str = "leg_qty * sell_weight * price (last sell = full leg_qty)"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundTruthModel:
|
||||
"""
|
||||
GT 전체 모델 (타점 + 비중 + 자본).
|
||||
|
||||
Attributes:
|
||||
selection_mode: 타점 생성 모드.
|
||||
entry: 매수 명세.
|
||||
exit: 매도 명세.
|
||||
capital: 자본 배분 명세.
|
||||
leg: leg 정의.
|
||||
"""
|
||||
|
||||
selection_mode: str = GT_SELECTION_MODE
|
||||
entry: GtEntrySpec = field(default_factory=GtEntrySpec)
|
||||
exit: GtExitSpec = field(default_factory=GtExitSpec)
|
||||
capital: GtCapitalSpec = field(default_factory=GtCapitalSpec)
|
||||
leg_definition: str = (
|
||||
"이전 고점 매도 ~ 다음 고점 매도 구간 = leg_id; "
|
||||
"기간말 잔여 구간은 마지막 leg"
|
||||
)
|
||||
execution_order_chrono: str = (
|
||||
"amount_krw 배분·summary.pnl_pct = 시각순 체결(매도 후 현금 → 다음 매수 반영)"
|
||||
)
|
||||
execution_order_leg_block: str = (
|
||||
"JSON 저장 순서 = leg별 매수 전량 → 매도 전량 (차트·테이블 leg 정합)"
|
||||
)
|
||||
|
||||
|
||||
def default_model() -> GroundTruthModel:
|
||||
"""현재 config 기준 GT 모델."""
|
||||
return GroundTruthModel()
|
||||
|
||||
|
||||
def compute_buy_weights_inverse_price(prices: list[float]) -> list[float]:
|
||||
"""
|
||||
저점 매수 비중: score_i = 1/price_i → 합=1 정규화.
|
||||
|
||||
Args:
|
||||
prices: leg 내 매수 후보 가격.
|
||||
|
||||
Returns:
|
||||
weight 리스트 (합 ≈ 1).
|
||||
"""
|
||||
if not prices:
|
||||
return []
|
||||
scores = [1.0 / max(p, 1e-9) for p in prices]
|
||||
s = sum(scores)
|
||||
return [x / s for x in scores] if s > 0 else [1.0 / len(prices)] * len(prices)
|
||||
|
||||
|
||||
def sell_split_weights(n_sells: int) -> list[float]:
|
||||
"""
|
||||
leg 매도 비중.
|
||||
|
||||
Args:
|
||||
n_sells: 매도 횟수(1 또는 2).
|
||||
|
||||
Returns:
|
||||
weight 리스트.
|
||||
"""
|
||||
spec = GtExitSpec()
|
||||
if n_sells >= 2:
|
||||
return list(spec.split_weights)
|
||||
return [1.0]
|
||||
|
||||
|
||||
def model_to_dict(model: GroundTruthModel | None = None) -> dict[str, Any]:
|
||||
"""
|
||||
JSON·리포트용 모델 dict.
|
||||
|
||||
Args:
|
||||
model: None이면 default.
|
||||
|
||||
Returns:
|
||||
직렬화 dict.
|
||||
"""
|
||||
m = model or default_model()
|
||||
return {
|
||||
"selection_mode": m.selection_mode,
|
||||
"leg_definition": m.leg_definition,
|
||||
"entry": {
|
||||
"pivot": m.entry.pivot_kind,
|
||||
"price": m.entry.price_field,
|
||||
"weight_rule": m.entry.weight_rule,
|
||||
"weight_formula": "w_i = (1/price_i) / sum(1/price_j)",
|
||||
"max_buys_per_leg": m.entry.max_per_leg,
|
||||
"min_bars_between_buys": m.entry.min_bars_gap,
|
||||
"bb_filter": m.entry.bb_filter,
|
||||
},
|
||||
"exit": {
|
||||
"pivot": m.exit.pivot_kind,
|
||||
"price": m.exit.price_field,
|
||||
"weight_rule": "fixed_split_or_full",
|
||||
"weights_two_sell": list(m.exit.split_weights),
|
||||
"split_gap_pct": m.exit.split_gap_pct,
|
||||
"max_sells_per_leg": m.exit.max_per_leg,
|
||||
},
|
||||
"capital": {
|
||||
"buy": m.capital.buy_formula,
|
||||
"optimal_buy_rate": m.capital.optimal_buy_rate,
|
||||
"large_leg_top_pct": m.capital.large_leg_top_pct,
|
||||
"pct_large_leg": m.capital.pct_large,
|
||||
"pct_small_leg": m.capital.pct_small,
|
||||
"min_order_krw": m.capital.min_order_krw,
|
||||
"sell": m.capital.sell_formula,
|
||||
},
|
||||
"execution": {
|
||||
"chrono": m.execution_order_chrono,
|
||||
"leg_block_json": m.execution_order_leg_block,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def summarize_leg_weights(trades: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""
|
||||
leg별 매수·매도 비중 합 검증용 요약.
|
||||
|
||||
Args:
|
||||
trades: GT trade dict.
|
||||
|
||||
Returns:
|
||||
leg_id → {buy_sum, sell_sum, n_buy, n_sell}.
|
||||
"""
|
||||
legs: dict[int, dict[str, Any]] = {}
|
||||
for t in trades:
|
||||
lid = int(t.get("leg_id", 0))
|
||||
legs.setdefault(
|
||||
lid,
|
||||
{"buy_sum": 0.0, "sell_sum": 0.0, "n_buy": 0, "n_sell": 0},
|
||||
)
|
||||
w = float(t.get("weight", 0))
|
||||
if t.get("action") == "buy":
|
||||
legs[lid]["buy_sum"] += w
|
||||
legs[lid]["n_buy"] += 1
|
||||
elif t.get("action") == "sell":
|
||||
legs[lid]["sell_sum"] += w
|
||||
legs[lid]["n_sell"] += 1
|
||||
return legs
|
||||
Reference in New Issue
Block a user