인과적 GT 신호·복리 배분 시뮬을 도입하고 운영 정합성을 맞춘다.
미래 데이터를 쓰지 않는 causal 신호/tier와 전기간 복리 포트폴리오 비교로 GT 대비 sim_sized 검증 경로를 정리하고, 일한도·매수 상한·live_buy 스케일을 제거한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -33,7 +33,6 @@ from config import (
|
||||
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,
|
||||
@@ -49,6 +48,16 @@ from config import (
|
||||
from deepcoin.common.indicators import apply_bar_indicators, get_trend
|
||||
from deepcoin.data.mtf_bb import load_frames_from_db
|
||||
|
||||
from deepcoin.ground_truth.gt_allocation import (
|
||||
allocate_order_amounts_chronological,
|
||||
resolve_sell_qty as _resolve_sell_qty,
|
||||
)
|
||||
from deepcoin.ground_truth.gt_model import (
|
||||
compute_entry_weights,
|
||||
leg_entry_weights,
|
||||
leg_exit_weights,
|
||||
sell_split_weights,
|
||||
)
|
||||
from deepcoin.paths import resolve_ground_truth_file
|
||||
|
||||
DEFAULT_OUTPUT = resolve_ground_truth_file()
|
||||
@@ -476,15 +485,6 @@ def _row_at_ts(df: pd.DataFrame, ts: pd.Timestamp) -> pd.Series:
|
||||
return row
|
||||
|
||||
|
||||
def _normalize_weights(scores: list[float]) -> list[float]:
|
||||
"""비중 점수를 합 1로 정규화."""
|
||||
total = sum(scores)
|
||||
if total <= 0:
|
||||
n = len(scores)
|
||||
return [1.0 / n] * n if n else []
|
||||
return [s / total for s in scores]
|
||||
|
||||
|
||||
def _collect_buy_troughs(
|
||||
df: pd.DataFrame,
|
||||
buy_pivots: list[Pivot],
|
||||
@@ -525,7 +525,6 @@ def _collect_buy_troughs(
|
||||
filtered.append(p)
|
||||
|
||||
if len(filtered) > max_buys:
|
||||
# 가격이 낮은(저점) 순으로 max_buys만 유지 후 시간순
|
||||
filtered.sort(key=lambda x: x.price)
|
||||
filtered = sorted(filtered[:max_buys], key=lambda x: x.ts)
|
||||
return filtered
|
||||
@@ -570,7 +569,8 @@ def _peak_sell_points(
|
||||
second = max(sub_peaks, key=lambda x: x.price)
|
||||
if second.ts == main.ts:
|
||||
return [(main, 1.0)]
|
||||
return [(main, 0.65), (second, 0.35)]
|
||||
w = sell_split_weights(2)
|
||||
return [(main, w[0]), (second, w[1])]
|
||||
|
||||
|
||||
def build_split_buy_peak_sell_trades(
|
||||
@@ -603,8 +603,11 @@ def build_split_buy_peak_sell_trades(
|
||||
for leg_id, peak in enumerate(sell_peaks):
|
||||
troughs = _collect_buy_troughs(df, buy_pivots, prev_sell_ts, peak.ts, buy_min_bars)
|
||||
if troughs:
|
||||
scores = [1.0 / max(t.price, 1e-9) for t in troughs]
|
||||
weights = _normalize_weights(scores)
|
||||
prices = [
|
||||
float(_row_at_ts(df, t.ts)["Low"]) if "Low" in _row_at_ts(df, t.ts) else t.price
|
||||
for t in troughs
|
||||
]
|
||||
weights = leg_entry_weights(prices)
|
||||
for t, w in zip(troughs, weights):
|
||||
row = _row_at_ts(df, t.ts)
|
||||
bb_pos, rsi, disp = _bb_context(row)
|
||||
@@ -672,7 +675,11 @@ def build_split_buy_peak_sell_trades(
|
||||
)
|
||||
leg_id = len(sell_peaks)
|
||||
if troughs:
|
||||
weights = _normalize_weights([1.0 / max(t.price, 1e-9) for t in troughs])
|
||||
prices = [
|
||||
float(_row_at_ts(df, t.ts)["Low"]) if "Low" in _row_at_ts(df, t.ts) else t.price
|
||||
for t in troughs
|
||||
]
|
||||
weights = leg_entry_weights(prices)
|
||||
leg_buys: list[TradePoint] = []
|
||||
for t, w in zip(troughs, weights):
|
||||
row = _row_at_ts(df, t.ts)
|
||||
@@ -916,7 +923,6 @@ def generate_ground_truth(
|
||||
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,
|
||||
@@ -963,13 +969,12 @@ 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 비중 share×티어 스케일), 체결=min(목표, 보유현금/(1+fee)).
|
||||
leg 상위 GT_LARGE_LEG_TOP_PCT는 GT_BUY_PCT_LARGE_LEG, 그 외는 GT_BUY_PCT_SMALL_LEG.
|
||||
매도 후 현금 증가분은 다음 매수부터 자동 반영(시각순 복리).
|
||||
|
||||
@@ -977,159 +982,18 @@ def allocate_gt_order_amounts(
|
||||
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,
|
||||
return allocate_order_amounts_chronological(
|
||||
trades,
|
||||
initial_cash=initial_cash,
|
||||
min_order_krw=min_order_krw,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
|
||||
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],
|
||||
|
||||
Reference in New Issue
Block a user