로고스 전략 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:
2026-05-29 19:07:10 +09:00
parent e218a8ea32
commit e631a5701f
12 changed files with 1639 additions and 100 deletions

View File

@@ -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