로고스 전략 FSM을 simulation 기본 실행에 통합한다.
수동 타점(logos_trades.json) 흐름에 맞춘 순차 매매 로직을 추가하고, python simulation.py 실행 시 로고스 백테스트·HTML을 생성한다. 규칙 탐색·BB 안전장치 개선과 함께 reports HTML은 gitignore로 제외한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
304
simulation.py
304
simulation.py
@@ -25,6 +25,7 @@ from plotly.subplots import make_subplots
|
||||
from config import (
|
||||
BUY_COOLDOWN_SEC,
|
||||
COIN_NAME,
|
||||
DEFAULT_BUY_KRW,
|
||||
ENTRY_INTERVAL,
|
||||
SELL_COOLDOWN_SEC,
|
||||
SIM_INITIAL_CASH_KRW,
|
||||
@@ -39,6 +40,8 @@ import strategy
|
||||
|
||||
REPORT_DIR = Path(__file__).resolve().parent / "reports"
|
||||
OUTPUT_HTML = REPORT_DIR / "wld_bb_simulation.html"
|
||||
LOGOS_BENCHMARK_HTML = REPORT_DIR / "wld_logos_benchmark.html"
|
||||
LOGOS_TRADES_FILE = Path(__file__).resolve().parent / "logos_trades.json"
|
||||
|
||||
|
||||
def interval_chart_label(interval_min: int) -> str:
|
||||
@@ -48,9 +51,28 @@ def interval_chart_label(interval_min: int) -> str:
|
||||
return f"{interval_min}분봉"
|
||||
|
||||
|
||||
def _format_trade_dt(dt: pd.Timestamp) -> str:
|
||||
"""마커 호버용 날짜·시간 문자열."""
|
||||
return pd.Timestamp(dt).strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def _trade_hover_lines(t: "SimTrade", label: str) -> str:
|
||||
"""매수·매도 마커 호버(팝업) 본문."""
|
||||
lines = [
|
||||
label,
|
||||
_format_trade_dt(t.dt),
|
||||
t.signal or "",
|
||||
f"₩{t.price:,.2f}",
|
||||
f"₩{t.krw:,.0f}",
|
||||
]
|
||||
if t.pnl is not None:
|
||||
lines.append(f"손익 ₩{t.pnl:+,.0f}")
|
||||
return "<br>".join(line for line in lines if line)
|
||||
|
||||
|
||||
def _add_trade_markers(fig, trades: list["SimTrade"], row: int = 1) -> None:
|
||||
"""
|
||||
매수·매도 마커·라벨 (Scatter trace만 사용, 범례와 함께 토글).
|
||||
매수·매도 체결 마커·라벨 (Scatter trace만 사용, 범례와 함께 토글).
|
||||
simulate_mtf.py 와 동일 스타일.
|
||||
"""
|
||||
for action, color, symbol, label, text_pos in [
|
||||
@@ -80,10 +102,7 @@ def _add_trade_markers(fig, trades: list["SimTrade"], row: int = 1) -> None:
|
||||
color=color,
|
||||
line=dict(width=2, color="#111"),
|
||||
),
|
||||
hovertext=[
|
||||
f"{label} 체결<br>{t.signal}<br>₩{t.price:,.2f}<br>₩{t.krw:,.0f}"
|
||||
for t in pts
|
||||
],
|
||||
hovertext=[_trade_hover_lines(t, label) for t in pts],
|
||||
hovertemplate="%{hovertext}<extra></extra>",
|
||||
),
|
||||
row=row,
|
||||
@@ -97,6 +116,8 @@ def build_simulation_html(
|
||||
trend: str,
|
||||
interval_min: int = ENTRY_INTERVAL,
|
||||
note: str = "",
|
||||
title_suffix: str = "BB 시뮬레이션",
|
||||
show_memo_column: bool = False,
|
||||
) -> str:
|
||||
"""simulate_mtf.py 와 동일 레이아웃의 HTML 리포트."""
|
||||
df = strategy.prepare_entry_df(df.copy())
|
||||
@@ -243,7 +264,16 @@ def build_simulation_html(
|
||||
for t in result.trades:
|
||||
cls = "buy" if t.action == "매수" else "sell"
|
||||
pnl = f"{t.pnl:+,.0f}" if t.pnl is not None else "-"
|
||||
trade_rows += f"""
|
||||
if show_memo_column:
|
||||
trade_rows += f"""
|
||||
<tr>
|
||||
<td>{t.dt}</td>
|
||||
<td class="{cls}">{t.action}</td>
|
||||
<td>₩{t.price:,.0f}</td>
|
||||
<td>{t.signal}</td>
|
||||
</tr>"""
|
||||
else:
|
||||
trade_rows += f"""
|
||||
<tr>
|
||||
<td>{t.dt}</td>
|
||||
<td class="{cls}">{t.action}</td>
|
||||
@@ -255,19 +285,31 @@ def build_simulation_html(
|
||||
<td>{pnl}</td>
|
||||
</tr>"""
|
||||
if not trade_rows:
|
||||
trade_rows = '<tr><td colspan="8">신호 없음</td></tr>'
|
||||
colspan = 4 if show_memo_column else 8
|
||||
trade_rows = f'<tr><td colspan="{colspan}">체결 없음</td></tr>'
|
||||
|
||||
note_html = f"<p class='warn'>{summary['note']}</p>" if summary.get("note") else ""
|
||||
sells = summary["sell_signal_count"]
|
||||
win_rate = (
|
||||
summary["win_count"] / sells * 100 if sells else 0.0
|
||||
)
|
||||
if show_memo_column:
|
||||
table_title = "로고스 타점 (직관 해석)"
|
||||
table_head = (
|
||||
"<th>시각</th><th>구분</th><th>가격</th><th>해석</th>"
|
||||
)
|
||||
else:
|
||||
table_title = "체결 내역"
|
||||
table_head = (
|
||||
"<th>시각</th><th>구분</th><th>상태</th><th>가격</th>"
|
||||
"<th>신호</th><th>금액</th><th>수수료</th><th>손익</th>"
|
||||
)
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>{SYMBOL} BB 시뮬레이션</title>
|
||||
<title>{SYMBOL} {title_suffix}</title>
|
||||
<style>
|
||||
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; background: #f8fafc; }}
|
||||
h1 {{ font-size: 1.35rem; }}
|
||||
@@ -290,7 +332,7 @@ def build_simulation_html(
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{COIN_NAME} ({SYMBOL}) BB 시뮬레이션</h1>
|
||||
<h1>{COIN_NAME} ({SYMBOL}) {title_suffix}</h1>
|
||||
<p class="meta">전략: {summary['config_name']} | 추세: {summary['trend']} | 기간: {summary['period_start']} ~ {summary['period_end']}</p>
|
||||
{note_html}
|
||||
<div class="legend-box">
|
||||
@@ -304,9 +346,9 @@ def build_simulation_html(
|
||||
<div class="card"><span>승률(매도 기준)</span><b>{win_rate:.1f}%</b></div>
|
||||
</div>
|
||||
<div class="chart-wrap">{chart_html}</div>
|
||||
<h2>신호·체결 내역</h2>
|
||||
<h2>{table_title}</h2>
|
||||
<table>
|
||||
<thead><tr><th>시각</th><th>구분</th><th>상태</th><th>가격</th><th>신호</th><th>금액</th><th>수수료</th><th>손익</th></tr></thead>
|
||||
<thead><tr>{table_head}</tr></thead>
|
||||
<tbody>{trade_rows}</tbody>
|
||||
</table>
|
||||
</body>
|
||||
@@ -364,6 +406,7 @@ def run_backtest(
|
||||
last_sell_ts: pd.Timestamp | None = None
|
||||
|
||||
signals = df_3m[df_3m["point"] == 1].sort_index()
|
||||
signals = signals[signals["action"].isin(["buy", "sell"])]
|
||||
|
||||
for ts, row in signals.iterrows():
|
||||
price = float(row["Close"])
|
||||
@@ -610,6 +653,194 @@ class Simulation:
|
||||
webbrowser.open(OUTPUT_HTML.resolve().as_uri())
|
||||
return OUTPUT_HTML
|
||||
|
||||
def render_logos_html(
|
||||
self,
|
||||
df_plot: pd.DataFrame,
|
||||
result: SimResult,
|
||||
trend: str,
|
||||
note: str = "",
|
||||
open_browser: bool = True,
|
||||
) -> Path:
|
||||
"""로고스 직관 타점 HTML (규칙 엔진 미사용)."""
|
||||
html = build_simulation_html(
|
||||
df_plot,
|
||||
result,
|
||||
trend,
|
||||
note=note,
|
||||
title_suffix="로고스 직관 타점 (3분봉)",
|
||||
show_memo_column=True,
|
||||
)
|
||||
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LOGOS_BENCHMARK_HTML.write_text(html, encoding="utf-8")
|
||||
print(f"HTML: {LOGOS_BENCHMARK_HTML}")
|
||||
if open_browser:
|
||||
webbrowser.open(LOGOS_BENCHMARK_HTML.resolve().as_uri())
|
||||
return LOGOS_BENCHMARK_HTML
|
||||
|
||||
def run_logos_strategy_chart(self, frames: dict[int, pd.DataFrame]) -> SimResult:
|
||||
"""
|
||||
로고스 전략(logos_strategy.py) 백테스트·HTML.
|
||||
|
||||
설계: docs/LOGOS_STRATEGY.md
|
||||
"""
|
||||
from candle_features import build_master_feature_matrix
|
||||
from logos_strategy import generate_logos_events
|
||||
|
||||
df_1d, df_1h, df_3m = self._frames_to_mtf(frames)
|
||||
trend = strategy.get_trend(df_1d, df_1h)
|
||||
matrix = build_master_feature_matrix(frames).iloc[21:].copy()
|
||||
df_sig = strategy.prepare_entry_df(df_3m)
|
||||
df_sig["signal"] = ""
|
||||
df_sig["point"] = 0
|
||||
df_sig["action"] = ""
|
||||
df_sig["trend"] = ""
|
||||
|
||||
logos_events = generate_logos_events(matrix, df_1d, df_1h)
|
||||
for ts, action, sig in logos_events:
|
||||
if ts not in df_sig.index:
|
||||
continue
|
||||
df_sig.at[ts, "signal"] = sig
|
||||
df_sig.at[ts, "point"] = 1
|
||||
df_sig.at[ts, "action"] = action
|
||||
df_sig.at[ts, "trend"] = strategy.get_trend_at(df_1d, df_1h, ts)
|
||||
|
||||
n_sig = int((df_sig["point"] == 1).sum())
|
||||
print(f"\n[로고스 전략] 체결 {n_sig}건")
|
||||
from logos_strategy import compare_to_ground_truth
|
||||
|
||||
cmp_rows = compare_to_ground_truth(logos_events, matrix)
|
||||
if cmp_rows:
|
||||
matched = sum(1 for r in cmp_rows if r["match"])
|
||||
print(f"[정답 비교] {matched}/{len(cmp_rows)} 타점 일치 (±20봉·±6%)")
|
||||
for r in cmp_rows:
|
||||
mark = "O" if r["match"] else "X"
|
||||
cand = r.get("cand_dt") or "-"
|
||||
print(
|
||||
f" [{mark}] {r['gt_action']} {r['gt_dt'][:16]} "
|
||||
f"-> {str(cand)[:16]} ({r['score_pct']}%)"
|
||||
)
|
||||
result = run_backtest(df_sig, df_1d, df_1h, config_name="logos_strategy")
|
||||
print_backtest_report(result)
|
||||
|
||||
html = build_simulation_html(
|
||||
df_sig,
|
||||
result,
|
||||
trend,
|
||||
note="로고스 전략: docs/LOGOS_STRATEGY.md (A바닥/B눌림/C익절/D과열)",
|
||||
title_suffix="로고스 전략 (3분봉)",
|
||||
)
|
||||
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT_HTML.write_text(html, encoding="utf-8")
|
||||
print(f"HTML: {OUTPUT_HTML}")
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open(OUTPUT_HTML.resolve().as_uri())
|
||||
return result
|
||||
|
||||
def run_logos_chart(self, frames: dict[int, pd.DataFrame]) -> SimResult:
|
||||
"""
|
||||
차트 해석 기반 수동 타점을 전 기간 3분봉에 표시합니다.
|
||||
|
||||
데이터: logos_trades.json (BB/탐색 규칙과 무관)
|
||||
"""
|
||||
import json
|
||||
|
||||
if not LOGOS_TRADES_FILE.exists():
|
||||
raise FileNotFoundError(f"{LOGOS_TRADES_FILE} 없음")
|
||||
|
||||
spec = json.loads(LOGOS_TRADES_FILE.read_text(encoding="utf-8"))
|
||||
df_1d, df_1h, df_3m = self._frames_to_mtf(frames)
|
||||
df_3m = strategy.prepare_entry_df(df_3m)
|
||||
trend = strategy.get_trend(df_1d, df_1h)
|
||||
|
||||
buy_krw = float(DEFAULT_BUY_KRW)
|
||||
fee_rate = TRADING_FEE_RATE
|
||||
cash = float(SIM_INITIAL_CASH_KRW)
|
||||
qty = 0.0
|
||||
cost = 0.0
|
||||
sim_trades: list[SimTrade] = []
|
||||
win = 0
|
||||
|
||||
for row in spec.get("trades") or []:
|
||||
ts = pd.Timestamp(row["dt"])
|
||||
act = row["action"]
|
||||
px = float(row["price"])
|
||||
memo = str(row.get("memo", ""))
|
||||
if act == "buy":
|
||||
fee = buy_krw * fee_rate
|
||||
cash -= buy_krw + fee
|
||||
qty += buy_krw / px
|
||||
cost += buy_krw
|
||||
sim_trades.append(
|
||||
SimTrade(
|
||||
dt=ts,
|
||||
action="매수",
|
||||
signal=memo,
|
||||
price=px,
|
||||
krw=buy_krw,
|
||||
fee=fee,
|
||||
quantity=buy_krw / px,
|
||||
pnl=None,
|
||||
cash_after=cash,
|
||||
total_asset=cash + qty * px,
|
||||
)
|
||||
)
|
||||
elif act == "sell" and qty > 0:
|
||||
sell_krw = qty * px
|
||||
fee = sell_krw * fee_rate
|
||||
net = sell_krw - fee
|
||||
pnl = net - cost
|
||||
if pnl > 0:
|
||||
win += 1
|
||||
cash += net
|
||||
sim_trades.append(
|
||||
SimTrade(
|
||||
dt=ts,
|
||||
action="매도",
|
||||
signal=memo,
|
||||
price=px,
|
||||
krw=sell_krw,
|
||||
fee=fee,
|
||||
quantity=qty,
|
||||
pnl=pnl,
|
||||
cash_after=cash,
|
||||
total_asset=cash,
|
||||
)
|
||||
)
|
||||
qty = 0.0
|
||||
cost = 0.0
|
||||
|
||||
last_px = float(df_3m["Close"].iloc[-1])
|
||||
result = SimResult(
|
||||
config_name=spec.get("name", "logos"),
|
||||
trades=sim_trades,
|
||||
initial_cash=SIM_INITIAL_CASH_KRW,
|
||||
final_cash=cash,
|
||||
final_coin_qty=qty,
|
||||
final_price=last_px,
|
||||
realized_pnl=sum(t.pnl or 0 for t in sim_trades if t.pnl),
|
||||
total_fees=sum(t.fee for t in sim_trades),
|
||||
final_asset=cash + qty * last_px,
|
||||
total_return_pct=(cash + qty * last_px - SIM_INITIAL_CASH_KRW)
|
||||
/ SIM_INITIAL_CASH_KRW
|
||||
* 100,
|
||||
trade_count=len(sim_trades),
|
||||
win_count=win,
|
||||
)
|
||||
|
||||
print(f"\n[로고스 직관 타점] {spec.get('author', 'Logos')}")
|
||||
print(f" 기간: {df_3m.index[0]} ~ {df_3m.index[-1]}")
|
||||
print(f" 타점 {len(sim_trades)}개 (규칙 엔진·BB 조건 미사용)")
|
||||
for t in sim_trades:
|
||||
print(f" {t.dt} {t.action} ₩{t.price:,.0f} — {t.signal}")
|
||||
print_backtest_report(result)
|
||||
|
||||
note = spec.get("note", "")
|
||||
path = self.render_logos_html(df_3m, result, trend, note=note, open_browser=True)
|
||||
print(f"\n차트 파일: {path.resolve()}")
|
||||
print("브라우저에서 열리지 않으면 위 경로를 더블클릭하세요.")
|
||||
return result
|
||||
|
||||
def load_all_frames(self) -> dict[int, pd.DataFrame]:
|
||||
"""discovered 규칙용 전 간격 로드."""
|
||||
from mtf_bb import load_frames_from_db
|
||||
@@ -690,7 +921,7 @@ class Simulation:
|
||||
n_sig = int((df_sig["point"] == 1).sum())
|
||||
buy_sig = int((df_sig["action"] == "buy").sum())
|
||||
sell_sig = int((df_sig["action"] == "sell").sum())
|
||||
print(f"\n규칙 신호: {n_sig} (매수 {buy_sig} / 매도 {sell_sig})")
|
||||
print(f"\n체결 신호: {n_sig} (매수 {buy_sig} / 매도 {sell_sig})")
|
||||
|
||||
result = run_backtest(df_sig, df_1d, df_1h, config_name=rule_set.name)
|
||||
print_backtest_report(result)
|
||||
@@ -770,7 +1001,15 @@ def run_analyze(frames: dict[int, pd.DataFrame] | None = None) -> None:
|
||||
|
||||
|
||||
def run_discover(frames: dict[int, pd.DataFrame] | None = None):
|
||||
"""모든 봉·BB·일목 특징으로 최적 규칙 탐색 후 JSON 저장."""
|
||||
"""
|
||||
모든 봉·BB·일목 특징으로 최적 규칙 탐색 후 JSON 저장.
|
||||
|
||||
Args:
|
||||
frames: 전 간격 OHLCV. None이면 coins.db에서 로드.
|
||||
|
||||
Returns:
|
||||
DiscoveredRules 또는 데이터/탐색 실패 시 None.
|
||||
"""
|
||||
from rule_discovery import discover_rules, save_rules
|
||||
|
||||
if frames is None:
|
||||
@@ -784,30 +1023,25 @@ def run_discover(frames: dict[int, pd.DataFrame] | None = None):
|
||||
return rules
|
||||
|
||||
|
||||
def run_full_pipeline() -> None:
|
||||
"""
|
||||
일반 사용자용 일괄 실행: analyze → discover → HTML.
|
||||
def run_logos_benchmark() -> None:
|
||||
"""수동 벤치마크 타점 차트 (logos_trades.json, 참고용)."""
|
||||
print("=== 로고스 수동 벤치마크 차트 ===")
|
||||
frames = _load_all_frames_or_exit()
|
||||
if frames is None:
|
||||
return
|
||||
Simulation().run_logos_chart(frames)
|
||||
print("\n완료.")
|
||||
|
||||
DB 로드는 한 번만 수행합니다.
|
||||
"""
|
||||
|
||||
def run_full_pipeline() -> None:
|
||||
"""로고스 전략 백테스트·HTML (단일 진입점)."""
|
||||
print("=" * 60)
|
||||
print("전체 파이프라인: analyze → discover → HTML")
|
||||
print("로고스 전략: 백테스트 → HTML")
|
||||
print("=" * 60)
|
||||
frames = _load_all_frames_or_exit()
|
||||
if frames is None:
|
||||
return
|
||||
|
||||
print("\n[1/3] 조합 분석 (analyze)")
|
||||
run_analyze(frames)
|
||||
|
||||
print("\n[2/3] 규칙 탐색 (discover)")
|
||||
run_discover(frames)
|
||||
|
||||
print("\n[3/3] 백테스트·HTML 차트 (탐색 규칙 매수·매도)")
|
||||
if rules is None:
|
||||
print("규칙 탐색 실패 — HTML 생략")
|
||||
return
|
||||
Simulation().run_discovered_chart(frames, rules=rules)
|
||||
Simulation().run_logos_strategy_chart(frames)
|
||||
print("\n완료.")
|
||||
|
||||
|
||||
@@ -817,7 +1051,10 @@ def print_usage() -> None:
|
||||
DeepCoin simulation.py
|
||||
|
||||
python simulation.py
|
||||
analyze + discover + HTML (차트 = discovered_rules 매수·매도)
|
||||
로고스 전략 백테스트·HTML → reports/wld_bb_simulation.html
|
||||
|
||||
python simulation.py benchmark
|
||||
수동 벤치마크(logos_trades.json) → reports/wld_logos_benchmark.html
|
||||
|
||||
(고급) analyze | discover | compare | mtf
|
||||
"""
|
||||
@@ -840,6 +1077,9 @@ def main() -> None:
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "discover":
|
||||
run_discover()
|
||||
return
|
||||
if len(sys.argv) > 1 and sys.argv[1] in ("benchmark", "logos"):
|
||||
run_logos_benchmark()
|
||||
return
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "mtf":
|
||||
run_mtf_analysis()
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user