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

@@ -28,6 +28,8 @@ from config import (
GT_INITIAL_CASH_KRW,
GT_MARKER_SIZE_MAX,
GT_MARKER_SIZE_MIN,
GT_MAX_BUY_ORDER_KRW,
LIVE_ORDER_KRW,
MACD_FAST,
MACD_SIGNAL,
MACD_SLOW,
@@ -58,16 +60,94 @@ def interval_chart_label(interval_min: int) -> str:
return f"{interval_min}분봉"
def _marker_sizes(trades: list[dict], action: str) -> list[float]:
"""비중(weight, 0~1)에 비례한 삼각형 크기."""
pts = [t for t in trades if t.get("action") == action]
def _marker_hover_text(
label: str,
t: dict,
*,
default_order_krw: float | None = None,
extra_lines: list[str] | None = None,
) -> str:
"""
차트 마커 툴팁: 체결가(price)와 체결 원화(amount_krw)를 함께 표시.
Args:
label: 정답/시뮬 매수·매도 라벨.
t: trade dict (price, amount_krw, weight, memo …).
default_order_krw: amount_krw 없을 때 표시할 기본 원화(시뮬 고정 주문).
extra_lines: 툴팁 하단 추가 줄.
Returns:
hovertext HTML 줄바꿈 문자열.
"""
action = t.get("action", t.get("side", ""))
amt_label = "매수금액" if action == "buy" else "매도금액"
lines = [
label,
str(t.get("dt", ""))[:16],
f"체결가 ₩{float(t['price']):,.0f}",
]
ak = t.get("amount_krw")
if ak is not None and float(ak) > 0:
lines.append(f"{amt_label}{float(ak):,.0f}")
elif default_order_krw is not None and action == "buy":
lines.append(f"{amt_label}{float(default_order_krw):,.0f}")
else:
lines.append(f"{amt_label} (미배분)")
if t.get("weight") is not None:
lines.append(f"비중 {float(t.get('weight', 1)) * 100:.0f}%")
if extra_lines:
lines.extend(extra_lines)
memo = t.get("memo", "")
if memo:
lines.append(str(memo))
rule_id = t.get("rule_id", "")
if rule_id:
lines.append(str(rule_id))
return "<br>".join(lines)
def _trade_amount_krw(t: dict) -> float:
"""
마커 크기·툴팁용 체결 원화. amount_krw 없으면 비중×상한으로 추정.
Args:
t: trade dict.
Returns:
원화 금액(0 이상).
"""
ak = t.get("amount_krw")
if ak is not None and float(ak) > 0:
return float(ak)
return max(float(t.get("weight", 1.0)), 0.05) * float(GT_MAX_BUY_ORDER_KRW)
def _marker_sizes(pts: list[dict]) -> list[float]:
"""
체결 원화(amount_krw)에 비례한 삼각형 크기.
같은 trace(매수 또는 매도) 안에서 최소·최대 금액으로 선형 스케일.
Args:
pts: 동일 action의 trade dict 리스트.
Returns:
plotly marker size(diameter) 리스트.
"""
if not pts:
return []
lo, hi = float(GT_MARKER_SIZE_MIN), float(GT_MARKER_SIZE_MAX)
return [
lo + (hi - lo) * min(max(float(t.get("weight", 1.0)), 0.05), 1.0)
for t in pts
]
amounts = [_trade_amount_krw(t) for t in pts]
amin, amax = min(amounts), max(amounts)
sizes: list[float] = []
for amount in amounts:
if amax > amin:
ratio = (amount - amin) / (amax - amin)
else:
ratio = 0.5
ratio = max(ratio, 0.08)
sizes.append(lo + (hi - lo) * ratio)
return sizes
def _add_sim_markers(fig, trades: list[dict], row: int = 1) -> None:
@@ -101,9 +181,14 @@ def _add_sim_markers(fig, trades: list[dict], row: int = 1) -> None:
opacity=0.75,
),
hovertext=[
f"{label}<br>{t['dt'][:16]}<br>₩{float(t['price']):,.0f}"
f"<br>leg_gt {float(t.get('forward_ret_pct', 0)):+.2f}%"
f"<br>{t.get('rule_id', '')}"
_marker_hover_text(
label,
t,
default_order_krw=LIVE_ORDER_KRW,
extra_lines=[
f"leg_gt {float(t.get('forward_ret_pct', 0)):+.2f}%",
],
)
for t in pts
],
hovertemplate="%{hovertext}<extra></extra>",
@@ -114,7 +199,7 @@ def _add_sim_markers(fig, trades: list[dict], row: int = 1) -> None:
def _add_truth_markers(fig, trades: list[dict], row: int = 1) -> None:
"""정답 매수·매도 마커 (삼각형 크기 = 비중)."""
"""정답 매수·매도 마커 (삼각형 크기 = 체결 원화 금액)."""
for action, color, symbol, label in [
("buy", "#16a34a", "triangle-up", "정답 매수"),
("sell", "#dc2626", "triangle-down", "정답 매도"),
@@ -122,7 +207,7 @@ def _add_truth_markers(fig, trades: list[dict], row: int = 1) -> None:
pts = [t for t in trades if t.get("action") == action]
if not pts:
continue
sizes = _marker_sizes(trades, action)
sizes = _marker_sizes(pts)
fig.add_trace(
go.Scatter(
x=[pd.Timestamp(t["dt"]) for t in pts],
@@ -138,9 +223,7 @@ def _add_truth_markers(fig, trades: list[dict], row: int = 1) -> None:
line=dict(width=1.5, color="#111"),
),
hovertext=[
f"{label}<br>{t['dt'][:16]}<br>₩{t['price']:,.0f}"
f"<br>비중 {float(t.get('weight', 1))*100:.0f}%"
f"<br>{t.get('memo', '')}"
_marker_hover_text(label, t)
for t in pts
],
hovertemplate="%{hovertext}<extra></extra>",
@@ -446,7 +529,7 @@ def build_chart_html(
)
trade_table = f"""
<h2>정답 타점 (ground_truth)</h2>
<p class="meta">삼각형 크기 = 비중. 매수: 저점 분할 / 매도: 고점 1~2회.
<p class="meta">삼각형 크기 = 체결 금액(매수/매도 각각 min~max). 매수: 저점 분할 / 매도: 고점 1~2회.
총평가 = 체결 직후 현금 + 보유×체결가.{mark_note}</p>
<table>
<thead><tr><th>시각</th><th>구분</th><th>비중</th><th>가격</th><th>총 평가금액</th><th>해석</th></tr></thead>
@@ -471,8 +554,11 @@ def build_chart_html(
)
default_legend = (
"▲ <b>정답 매수</b> · ▼ <b>정답 매도</b> — 삼각형 크기 = 비중.<br>"
"● <b>시뮬 매수</b> · ● <b>시뮬 매도</b> — 원 = monitor_rules holdout 발화."
"▲ <b>정답 매수</b> · ▼ <b>정답 매도</b> — 크기=체결금액. "
"매수=총자산×비중×leg티어(상위 대형). "
"툴팁: 체결가·매수/매도금액.<br>"
"● <b>시뮬 매수</b> · ● <b>시뮬 매도</b> — 원 = holdout 발화 "
f"(매수 ₩{LIVE_ORDER_KRW:,.0f}/회)."
)
if cards_html:
cards_inner = cards_html