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:
@@ -42,6 +42,7 @@ from deepcoin.common.indicators import apply_bar_indicators, disparity_column, g
|
||||
from deepcoin.ops.monitor import Monitor
|
||||
from deepcoin.data.mtf_bb import interval_label, load_frames_from_db
|
||||
|
||||
from deepcoin.ops.chart_report import wrap_chart_report_page
|
||||
from deepcoin.paths import CHART_BB_HTML, CHART_TRUTH_HTML, resolve_ground_truth_file
|
||||
|
||||
OUTPUT_HTML = CHART_BB_HTML
|
||||
@@ -69,6 +70,49 @@ def _marker_sizes(trades: list[dict], action: str) -> list[float]:
|
||||
]
|
||||
|
||||
|
||||
def _add_sim_markers(fig, trades: list[dict], row: int = 1) -> None:
|
||||
"""
|
||||
시뮬(규칙) 매수·매도 마커 — 원형, GT 삼각형과 구분.
|
||||
|
||||
Args:
|
||||
fig: plotly Figure.
|
||||
trades: dt, action, price, forward_ret_pct, rule_id 키.
|
||||
row: subplot row.
|
||||
"""
|
||||
for action, color, label in [
|
||||
("buy", "#059669", "시뮬 매수"),
|
||||
("sell", "#b91c1c", "시뮬 매도"),
|
||||
]:
|
||||
pts = [t for t in trades if t.get("action") == action]
|
||||
if not pts:
|
||||
continue
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=[pd.Timestamp(t["dt"]) for t in pts],
|
||||
y=[float(t["price"]) for t in pts],
|
||||
mode="markers",
|
||||
name=label,
|
||||
legendgroup=label,
|
||||
marker=dict(
|
||||
symbol="circle",
|
||||
size=9,
|
||||
color=color,
|
||||
line=dict(width=1, color="#fff"),
|
||||
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', '')}"
|
||||
for t in pts
|
||||
],
|
||||
hovertemplate="%{hovertext}<extra></extra>",
|
||||
),
|
||||
row=row,
|
||||
col=1,
|
||||
)
|
||||
|
||||
|
||||
def _add_truth_markers(fig, trades: list[dict], row: int = 1) -> None:
|
||||
"""정답 매수·매도 마커 (삼각형 크기 = 비중)."""
|
||||
for action, color, symbol, label in [
|
||||
@@ -112,8 +156,12 @@ def build_chart_html(
|
||||
interval_min: int = ENTRY_INTERVAL,
|
||||
note: str = "",
|
||||
truth_trades: list[dict] | None = None,
|
||||
sim_trades: list[dict] | None = None,
|
||||
title_suffix: str = "BB 차트",
|
||||
pnl_summary: dict | None = None,
|
||||
legend_html: str | None = None,
|
||||
footer_sections: str | None = None,
|
||||
cards_html: str | None = None,
|
||||
) -> str:
|
||||
"""BB·이격도·RSI·MACD·스토캐스틱·거래량 차트 HTML."""
|
||||
df = apply_bar_indicators(df.copy())
|
||||
@@ -191,6 +239,8 @@ def build_chart_html(
|
||||
|
||||
if truth_trades:
|
||||
_add_truth_markers(fig, truth_trades, row=1)
|
||||
if sim_trades:
|
||||
_add_sim_markers(fig, sim_trades, row=1)
|
||||
|
||||
disp_row = 2
|
||||
for i, p in enumerate(DISPARITY_PERIODS):
|
||||
@@ -340,7 +390,8 @@ def build_chart_html(
|
||||
last_price=close_last,
|
||||
)
|
||||
trade_rows = ""
|
||||
if truth_trades:
|
||||
trade_table = footer_sections or ""
|
||||
if footer_sections is None and truth_trades:
|
||||
from deepcoin.ground_truth.ground_truth import simulate_truth_portfolio_steps
|
||||
|
||||
steps = simulate_truth_portfolio_steps(
|
||||
@@ -385,14 +436,13 @@ def build_chart_html(
|
||||
<td><b>{total_s}</b>{hold_s}</td>
|
||||
<td>{t.get('memo', '')}</td>
|
||||
</tr>"""
|
||||
trade_table = ""
|
||||
if truth_trades:
|
||||
if not trade_table and truth_trades:
|
||||
if not trade_rows:
|
||||
trade_rows = "<tr><td colspan='6'>타점 없음</td></tr>"
|
||||
mark_note = ""
|
||||
if pnl.get("mark_price"):
|
||||
mark_note = (
|
||||
f" 상단 최종 자산은 미청산 포함 종가 ₩{pnl['mark_price']:,.0f} 평가."
|
||||
f" 총보유자산(미청산 포함)은 종가 ₩{pnl['mark_price']:,.0f} 평가."
|
||||
)
|
||||
trade_table = f"""
|
||||
<h2>정답 타점 (ground_truth)</h2>
|
||||
@@ -405,54 +455,43 @@ def build_chart_html(
|
||||
|
||||
pnl_cards = ""
|
||||
if truth_trades and pnl.get("initial_cash_krw") is not None:
|
||||
pnl_cards = f"""
|
||||
<div class="card"><span>시작</span><b>₩{pnl['initial_cash_krw']:,.0f}</b></div>
|
||||
<div class="card"><span>최종 자산</span><b>₩{pnl['final_asset_krw']:,.0f}</b></div>
|
||||
<div class="card"><span>수익금</span><b>₩{pnl['pnl_krw']:+,.0f}</b></div>
|
||||
<div class="card"><span>수익률</span><b>{pnl['pnl_pct']:+.2f}%</b></div>
|
||||
<div class="card"><span>수수료</span><b>₩{pnl['total_fees_krw']:,.0f}</b></div>"""
|
||||
if pnl.get("holding_qty", 0) > 0:
|
||||
pnl_cards += f"""
|
||||
<div class="card"><span>미청산</span><b>{pnl['holding_qty']}개 (₩{pnl['holding_value_krw']:,.0f})</b></div>"""
|
||||
from deepcoin.ops.chart_report import card_html, initial_change_pct
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>{SYMBOL} {title_suffix}</title>
|
||||
<style>
|
||||
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; background: #f8fafc; }}
|
||||
h1 {{ font-size: 1.35rem; }}
|
||||
.meta {{ color: #475569; font-size: 0.9rem; }}
|
||||
.note {{ background: #f1f5f9; border: 1px solid #cbd5e1; padding: 10px; border-radius: 6px; color: #334155; }}
|
||||
.cards {{ display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0; }}
|
||||
.card {{ background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 14px; }}
|
||||
.card span {{ font-size: 0.75rem; color: #64748b; display: block; }}
|
||||
.card b {{ font-size: 1.05rem; }}
|
||||
.chart-wrap {{ background:#fff; border:1px solid #e2e8f0; border-radius:8px; padding:8px; }}
|
||||
.legend-box {{ font-size:0.85rem; color:#475569; margin-bottom:10px; }}
|
||||
table {{ width:100%; border-collapse:collapse; background:#fff; font-size:0.85rem; }}
|
||||
th, td {{ border:1px solid #e2e8f0; padding:8px; text-align:left; }}
|
||||
th {{ background:#f1f5f9; }}
|
||||
td.buy {{ color:#16a34a; font-weight:600; }}
|
||||
td.sell {{ color:#dc2626; font-weight:600; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{COIN_NAME} ({SYMBOL}) {title_suffix}</h1>
|
||||
<p class="meta">추세(참고): {trend} | 기간: {df.index[0]} ~ {df.index[-1]} | 봉 수: {len(df)}</p>
|
||||
{note_html}
|
||||
<div class="legend-box">▲ 매수 · ▼ 매도 — 삼각형이 클수록 비중이 큽니다.</div>
|
||||
<div class="cards">
|
||||
change_pct = initial_change_pct(pnl)
|
||||
pnl_cards = (
|
||||
card_html("초기 금액", f"₩{pnl['initial_cash_krw']:,.0f}")
|
||||
+ card_html("총보유자산", f"₩{pnl['final_asset_krw']:,.0f}")
|
||||
+ card_html("초기 대비 증감율", f"{change_pct:+.2f}%")
|
||||
+ card_html("수수료", f"₩{pnl['total_fees_krw']:,.0f}")
|
||||
)
|
||||
if pnl.get("holding_qty", 0) > 0:
|
||||
pnl_cards += card_html(
|
||||
"미청산",
|
||||
f"{pnl['holding_qty']}개 (₩{pnl['holding_value_krw']:,.0f})",
|
||||
)
|
||||
|
||||
default_legend = (
|
||||
"▲ <b>정답 매수</b> · ▼ <b>정답 매도</b> — 삼각형 크기 = 비중.<br>"
|
||||
"● <b>시뮬 매수</b> · ● <b>시뮬 매도</b> — 원 = monitor_rules holdout 발화."
|
||||
)
|
||||
if cards_html:
|
||||
cards_inner = cards_html
|
||||
else:
|
||||
cards_inner = f"""
|
||||
<div class="card"><span>종가</span><b>₩{close_last:,.2f}</b></div>
|
||||
<div class="card"><span>BB %B</span><b>{bb_pos_txt}</b></div>
|
||||
<div class="card"><span>정답 타점</span><b>{len(truth_trades) if truth_trades else 0}건</b></div>
|
||||
{pnl_cards}
|
||||
</div>
|
||||
<div class="chart-wrap">{chart_html}</div>
|
||||
{trade_table}
|
||||
</body>
|
||||
</html>"""
|
||||
{pnl_cards}"""
|
||||
return wrap_chart_report_page(
|
||||
page_title=f"{SYMBOL} {title_suffix}",
|
||||
heading=f"{COIN_NAME} ({SYMBOL}) {title_suffix}",
|
||||
meta_line=f"추세(참고): {trend} | 기간: {df.index[0]} ~ {df.index[-1]} | 봉 수: {len(df)}",
|
||||
note_html=note_html,
|
||||
legend_html=legend_html or default_legend,
|
||||
cards_html=cards_inner,
|
||||
chart_html=chart_html,
|
||||
sections_html=trade_table,
|
||||
)
|
||||
|
||||
|
||||
def _frames_to_mtf(
|
||||
|
||||
Reference in New Issue
Block a user