- strategy.py, candle_features.py, rule_discovery.py로 다봉 BB·캔들 규칙 탐색 - simulation_1h.py: discover 명령, 기본 BB vs 탐색 규칙 자동 선택, Plotly Y축 줌 - mtf_bb.py, downloader/monitor 정리, 다코인 파일 제거 Co-authored-by: Cursor <cursoragent@cursor.com>
672 lines
22 KiB
Python
672 lines
22 KiB
Python
"""
|
|
WLD 3분 BB 시뮬레이션.
|
|
|
|
기본: 하단 상향 돌파 매수, 상단 상향 돌파 매도.
|
|
수수료 반영, 레짐/필터 조합 비교 지원.
|
|
|
|
python simulation_1h.py # discovered_rules HTML 차트 (기본)
|
|
python simulation_1h.py discover # 모든 봉 특징 탐색 → discovered_rules.json
|
|
python simulation_1h.py compare # 9종 조합 수익률 순위
|
|
python simulation_1h.py mtf # 봉별 BB 비교 + MTF 시뮬
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from dataclasses import dataclass
|
|
|
|
import pandas as pd
|
|
import plotly.graph_objs as go
|
|
import plotly.io as pio
|
|
from datetime import datetime
|
|
from plotly import subplots
|
|
|
|
pio.renderers.default = "browser"
|
|
|
|
from config import (
|
|
BUY_COOLDOWN_SEC,
|
|
COIN_NAME,
|
|
ENTRY_INTERVAL,
|
|
SELL_COOLDOWN_SEC,
|
|
SIM_INITIAL_CASH_KRW,
|
|
SIM_MIN_ORDER_KRW,
|
|
SYMBOL,
|
|
TRADING_FEE_RATE,
|
|
TREND_INTERVAL_1D,
|
|
TREND_INTERVAL_1H,
|
|
)
|
|
from monitor import Monitor
|
|
import strategy
|
|
|
|
|
|
@dataclass
|
|
class SimTrade:
|
|
dt: pd.Timestamp
|
|
action: str
|
|
signal: str
|
|
price: float
|
|
krw: float
|
|
fee: float
|
|
quantity: float
|
|
pnl: float | None
|
|
cash_after: float
|
|
total_asset: float
|
|
|
|
|
|
@dataclass
|
|
class SimResult:
|
|
config_name: str
|
|
trades: list[SimTrade]
|
|
initial_cash: float
|
|
final_cash: float
|
|
final_coin_qty: float
|
|
final_price: float
|
|
realized_pnl: float
|
|
total_fees: float
|
|
final_asset: float
|
|
total_return_pct: float
|
|
trade_count: int
|
|
win_count: int
|
|
|
|
|
|
def run_backtest(
|
|
df_3m: pd.DataFrame,
|
|
df_1d: pd.DataFrame,
|
|
df_1h: pd.DataFrame,
|
|
config_name: str = "",
|
|
initial_cash: float = SIM_INITIAL_CASH_KRW,
|
|
min_order_krw: float = SIM_MIN_ORDER_KRW,
|
|
fee_rate: float = TRADING_FEE_RATE,
|
|
) -> SimResult:
|
|
"""신호 순서대로 현물 매수/매도 시뮬레이션 (수수료 차감)."""
|
|
cash = float(initial_cash)
|
|
coin_qty = 0.0
|
|
cost_basis = 0.0
|
|
realized_pnl = 0.0
|
|
total_fees = 0.0
|
|
win_count = 0
|
|
trades: list[SimTrade] = []
|
|
last_buy_ts: pd.Timestamp | None = None
|
|
last_sell_ts: pd.Timestamp | None = None
|
|
|
|
signals = df_3m[df_3m["point"] == 1].sort_index()
|
|
|
|
for ts, row in signals.iterrows():
|
|
price = float(row["Close"])
|
|
action = str(row.get("action", ""))
|
|
signal_name = str(row.get("signal", ""))
|
|
if price <= 0:
|
|
continue
|
|
|
|
trend_at = str(row.get("trend", "")) or strategy.get_trend_at(df_1d, df_1h, ts)
|
|
if trend_at not in ("up", "down", "range"):
|
|
trend_at = strategy.get_trend_at(df_1d, df_1h, ts)
|
|
|
|
if action == "buy":
|
|
if last_buy_ts is not None:
|
|
if (ts - last_buy_ts).total_seconds() < BUY_COOLDOWN_SEC:
|
|
continue
|
|
|
|
buy_krw = float(
|
|
strategy.get_buy_amount(SYMBOL, signal_name, price, trend_at)
|
|
)
|
|
buy_krw = max(min_order_krw, min(buy_krw, cash))
|
|
fee = buy_krw * fee_rate
|
|
total_cost = buy_krw + fee
|
|
if buy_krw < min_order_krw or cash < total_cost:
|
|
continue
|
|
|
|
qty = buy_krw / price
|
|
cash -= total_cost
|
|
total_fees += fee
|
|
cost_basis += buy_krw
|
|
coin_qty += qty
|
|
last_buy_ts = ts
|
|
|
|
trades.append(
|
|
SimTrade(
|
|
dt=ts,
|
|
action="매수",
|
|
signal=signal_name,
|
|
price=price,
|
|
krw=buy_krw,
|
|
fee=fee,
|
|
quantity=qty,
|
|
pnl=None,
|
|
cash_after=cash,
|
|
total_asset=cash + coin_qty * price,
|
|
)
|
|
)
|
|
continue
|
|
|
|
if action == "sell":
|
|
if coin_qty <= 0:
|
|
continue
|
|
if last_sell_ts is not None:
|
|
if (ts - last_sell_ts).total_seconds() < SELL_COOLDOWN_SEC:
|
|
continue
|
|
|
|
ratio = strategy.get_sell_ratio(SYMBOL, signal_name)
|
|
sell_qty = min(coin_qty * ratio, coin_qty)
|
|
sell_krw = sell_qty * price
|
|
|
|
if sell_krw < min_order_krw:
|
|
if coin_qty * price < min_order_krw:
|
|
continue
|
|
sell_qty = coin_qty
|
|
sell_krw = sell_qty * price
|
|
|
|
fee = sell_krw * fee_rate
|
|
net = sell_krw - fee
|
|
avg_cost = cost_basis / coin_qty
|
|
sold_cost = avg_cost * sell_qty
|
|
pnl = net - sold_cost
|
|
|
|
cash += net
|
|
total_fees += fee
|
|
cost_basis -= sold_cost
|
|
coin_qty -= sell_qty
|
|
realized_pnl += pnl
|
|
if pnl > 0:
|
|
win_count += 1
|
|
if coin_qty < 1e-12:
|
|
coin_qty = 0.0
|
|
cost_basis = 0.0
|
|
last_sell_ts = ts
|
|
|
|
trades.append(
|
|
SimTrade(
|
|
dt=ts,
|
|
action="매도",
|
|
signal=signal_name,
|
|
price=price,
|
|
krw=sell_krw,
|
|
fee=fee,
|
|
quantity=sell_qty,
|
|
pnl=pnl,
|
|
cash_after=cash,
|
|
total_asset=cash + coin_qty * price,
|
|
)
|
|
)
|
|
|
|
final_price = float(df_3m["Close"].iloc[-1])
|
|
final_asset = cash + coin_qty * final_price
|
|
sell_trades = sum(1 for t in trades if t.action == "매도")
|
|
|
|
return SimResult(
|
|
config_name=config_name,
|
|
trades=trades,
|
|
initial_cash=initial_cash,
|
|
final_cash=cash,
|
|
final_coin_qty=coin_qty,
|
|
final_price=final_price,
|
|
realized_pnl=realized_pnl,
|
|
total_fees=total_fees,
|
|
final_asset=final_asset,
|
|
total_return_pct=(final_asset - initial_cash) / initial_cash * 100
|
|
if initial_cash > 0
|
|
else 0.0,
|
|
trade_count=len(trades),
|
|
win_count=win_count if sell_trades else 0,
|
|
)
|
|
|
|
|
|
def print_backtest_report(result: SimResult) -> None:
|
|
fee_pct = TRADING_FEE_RATE * 100
|
|
print("\n" + "=" * 80)
|
|
print(
|
|
f"[{result.config_name}] 시작 {result.initial_cash:,.0f}원 | "
|
|
f"최소주문 {SIM_MIN_ORDER_KRW:,.0f}원 | 수수료 {fee_pct:.3f}%/쪽"
|
|
)
|
|
print("=" * 80)
|
|
if not result.trades:
|
|
print("체결 없음")
|
|
else:
|
|
print(
|
|
f"{'일시':<18} {'구분':<4} {'신호':<22} {'가격':>9} {'금액':>10} "
|
|
f"{'수수료':>8} {'수익':>10}"
|
|
)
|
|
print("-" * 80)
|
|
for t in result.trades:
|
|
pnl_s = f"{t.pnl:+,.0f}" if t.pnl is not None else "-"
|
|
print(
|
|
f"{t.dt.strftime('%Y-%m-%d %H:%M'):<18} {t.action:<4} {t.signal:<22} "
|
|
f"{t.price:>9,.2f} {t.krw:>10,.0f} {t.fee:>8,.0f} {pnl_s:>10}"
|
|
)
|
|
print("-" * 80)
|
|
sells = sum(1 for t in result.trades if t.action == "매도")
|
|
win_rate = result.win_count / sells * 100 if sells else 0.0
|
|
print(f"거래 횟수: {result.trade_count} (매도 {sells}회) | 승률: {win_rate:.1f}%")
|
|
print(f"수수료 합계: {result.total_fees:,.0f}원")
|
|
print(f"실현 손익(수수료 반영): {result.realized_pnl:+,.0f}원")
|
|
print(
|
|
f"최종 자산: {result.final_asset:,.0f}원 | "
|
|
f"총수익: {result.final_asset - result.initial_cash:+,.0f}원 "
|
|
f"({result.total_return_pct:+.2f}%)"
|
|
)
|
|
print("=" * 80)
|
|
|
|
|
|
def run_comparison(df_1d: pd.DataFrame, df_1h: pd.DataFrame, df_3m: pd.DataFrame) -> None:
|
|
"""기법 조합별 수익률 비교 (수수료 포함)."""
|
|
print(f"\n{'='*80}")
|
|
print(f"전략 조합 비교 — {SYMBOL} 3분 | {df_3m.index[0]} ~ {df_3m.index[-1]}")
|
|
print(f"시작 {SIM_INITIAL_CASH_KRW:,}원 | 수수료 {TRADING_FEE_RATE*100:.3f}%/매수·매도")
|
|
print(f"{'='*80}")
|
|
print(
|
|
f"{'순위':<4} {'조합':<22} {'수익률':>9} {'최종자산':>12} "
|
|
f"{'거래':>6} {'승률':>7} {'수수료':>10}"
|
|
)
|
|
print("-" * 80)
|
|
|
|
rows: list[tuple[SimResult, strategy.StrategyConfig]] = []
|
|
for cfg in strategy.comparison_presets():
|
|
df_sig = strategy.annotate_signals(
|
|
SYMBOL,
|
|
df_3m.copy(),
|
|
simulation=True,
|
|
df_1h=df_1h,
|
|
df_1d=df_1d,
|
|
config=cfg,
|
|
)
|
|
res = run_backtest(df_sig, df_1d, df_1h, config_name=cfg.name)
|
|
rows.append((res, cfg))
|
|
|
|
rows.sort(key=lambda x: x[0].total_return_pct, reverse=True)
|
|
|
|
for rank, (res, cfg) in enumerate(rows, 1):
|
|
sells = sum(1 for t in res.trades if t.action == "매도")
|
|
wr = res.win_count / sells * 100 if sells else 0.0
|
|
print(
|
|
f"{rank:<4} {res.config_name:<22} {res.total_return_pct:>+8.2f}% "
|
|
f"{res.final_asset:>12,.0f} {res.trade_count:>6} {wr:>6.1f}% "
|
|
f"{res.total_fees:>10,.0f}"
|
|
)
|
|
|
|
best_res, best_cfg = rows[0]
|
|
print("-" * 80)
|
|
print(f"1위: {best_cfg.name} ({best_res.total_return_pct:+.2f}%)")
|
|
print(
|
|
"실거래 적용: strategy.ACTIVE_CONFIG 를 1위 조합으로 맞추세요 "
|
|
"(현재 ACTIVE_CONFIG.name=%s)" % strategy.ACTIVE_CONFIG.name
|
|
)
|
|
print(f"{'='*80}\n")
|
|
|
|
|
|
class Simulation:
|
|
def __init__(self) -> None:
|
|
self.monitor = Monitor(cooldown_file=None)
|
|
|
|
def load_mtf(self, symbol: str):
|
|
df_1d = self.monitor.get_coin_some_data(symbol, TREND_INTERVAL_1D)
|
|
df_1h = self.monitor.get_coin_some_data(symbol, TREND_INTERVAL_1H)
|
|
df_3m = self.monitor.get_coin_some_data(symbol, ENTRY_INTERVAL)
|
|
|
|
if df_1d is None or df_1d.empty:
|
|
df_1d = self.monitor.get_coin_more_data(symbol, TREND_INTERVAL_1D, bong_count=500)
|
|
if df_1h is None or df_1h.empty:
|
|
df_1h = self.monitor.get_coin_more_data(symbol, TREND_INTERVAL_1H, bong_count=5000)
|
|
if df_3m is None or df_3m.empty:
|
|
df_3m = self.monitor.get_coin_more_data(
|
|
symbol, ENTRY_INTERVAL, bong_count=90000, verbose=True
|
|
)
|
|
|
|
df_1d = self.monitor.calculate_technical_indicators(df_1d)
|
|
df_1h = self.monitor.calculate_technical_indicators(df_1h)
|
|
df_3m = self.monitor.calculate_technical_indicators(df_3m)
|
|
return df_1d, df_1h, df_3m
|
|
|
|
def render_plotly(self, df_3m: pd.DataFrame, trend: str, result: SimResult) -> None:
|
|
cfg = strategy.ACTIVE_CONFIG.name
|
|
summary = (
|
|
f"[{cfg}] 시작 {result.initial_cash:,.0f} | 최종 {result.final_asset:,.0f} | "
|
|
f"{result.total_return_pct:+.2f}% | 수수료 {result.total_fees:,.0f}"
|
|
)
|
|
fig = subplots.make_subplots(
|
|
rows=3,
|
|
cols=1,
|
|
subplot_titles=(
|
|
f"{COIN_NAME} 3분 BB — {trend}",
|
|
"RSI / BB폭(%)",
|
|
summary,
|
|
),
|
|
shared_xaxes=False,
|
|
vertical_spacing=0.06,
|
|
row_heights=[0.5, 0.18, 0.32],
|
|
specs=[[{"type": "xy"}], [{"type": "xy"}], [{"type": "table"}]],
|
|
)
|
|
fig.add_trace(
|
|
go.Candlestick(
|
|
x=df_3m.index,
|
|
open=df_3m["Open"],
|
|
high=df_3m["High"],
|
|
low=df_3m["Low"],
|
|
close=df_3m["Close"],
|
|
name="캔들",
|
|
showlegend=False,
|
|
),
|
|
row=1,
|
|
col=1,
|
|
)
|
|
for col, color in [("MA", "blue"), ("Upper", "gray"), ("Lower", "gray")]:
|
|
if col in df_3m.columns:
|
|
fig.add_trace(
|
|
go.Scatter(
|
|
x=df_3m.index,
|
|
y=df_3m[col],
|
|
name=col,
|
|
line=dict(color=color, dash="dot" if col != "MA" else "solid"),
|
|
showlegend=False,
|
|
),
|
|
row=1,
|
|
col=1,
|
|
)
|
|
|
|
buy_trades = [t for t in result.trades if t.action == "매수"]
|
|
sell_trades = [t for t in result.trades if t.action == "매도"]
|
|
fig.add_trace(
|
|
go.Scatter(
|
|
x=[t.dt for t in buy_trades],
|
|
y=[t.price for t in buy_trades],
|
|
mode="markers",
|
|
name="매수",
|
|
legendgroup="trades",
|
|
showlegend=True,
|
|
marker=dict(
|
|
color="#22c55e",
|
|
size=11,
|
|
symbol="triangle-up",
|
|
line=dict(width=1, color="#166534"),
|
|
),
|
|
),
|
|
row=1,
|
|
col=1,
|
|
)
|
|
fig.add_trace(
|
|
go.Scatter(
|
|
x=[t.dt for t in sell_trades],
|
|
y=[t.price for t in sell_trades],
|
|
mode="markers",
|
|
name="매도",
|
|
legendgroup="trades",
|
|
showlegend=True,
|
|
marker=dict(
|
|
color="#ef4444",
|
|
size=11,
|
|
symbol="triangle-down",
|
|
line=dict(width=1, color="#991b1b"),
|
|
),
|
|
),
|
|
row=1,
|
|
col=1,
|
|
)
|
|
if "RSI" in df_3m.columns:
|
|
fig.add_trace(
|
|
go.Scatter(
|
|
x=df_3m.index,
|
|
y=df_3m["RSI"],
|
|
name="RSI",
|
|
showlegend=False,
|
|
),
|
|
row=2,
|
|
col=1,
|
|
)
|
|
if "BB_Width" in df_3m.columns:
|
|
fig.add_trace(
|
|
go.Scatter(
|
|
x=df_3m.index,
|
|
y=df_3m["BB_Width"],
|
|
name="BB폭%",
|
|
showlegend=False,
|
|
),
|
|
row=2,
|
|
col=1,
|
|
)
|
|
if result.trades:
|
|
cells = [
|
|
[t.dt.strftime("%Y-%m-%d %H:%M") for t in result.trades],
|
|
[t.action for t in result.trades],
|
|
[t.signal for t in result.trades],
|
|
[f"{t.price:,.2f}" for t in result.trades],
|
|
[f"{t.krw:,.0f}" for t in result.trades],
|
|
[f"{t.fee:,.0f}" for t in result.trades],
|
|
[f"{t.pnl:+,.0f}" if t.pnl is not None else "-" for t in result.trades],
|
|
[f"{t.total_asset:,.0f}" for t in result.trades],
|
|
]
|
|
else:
|
|
cells = [["-"] * 8]
|
|
fig.add_trace(
|
|
go.Table(
|
|
header=dict(
|
|
values=[
|
|
"일시",
|
|
"구분",
|
|
"신호",
|
|
"가격",
|
|
"금액",
|
|
"수수료",
|
|
"수익",
|
|
"총자산",
|
|
],
|
|
fill_color="#e8e8e8",
|
|
),
|
|
cells=dict(values=cells),
|
|
),
|
|
row=3,
|
|
col=1,
|
|
)
|
|
fig.update_layout(
|
|
height=1100,
|
|
title=f"{SYMBOL} BB 타이밍 시뮬 (범례 클릭: 매수/매도 표시 토글)",
|
|
margin=dict(l=50, r=140, t=80, b=40),
|
|
dragmode="zoom",
|
|
legend=dict(
|
|
orientation="v",
|
|
yanchor="top",
|
|
y=0.99,
|
|
xanchor="left",
|
|
x=1.01,
|
|
bgcolor="rgba(255,255,255,0.9)",
|
|
bordercolor="#cccccc",
|
|
borderwidth=1,
|
|
font=dict(size=12),
|
|
title=dict(text="체결 (클릭 토글)", side="top"),
|
|
itemclick="toggle",
|
|
itemdoubleclick="toggleothers",
|
|
),
|
|
)
|
|
# Y축 고정·rangeslider 해제 → 세로 드래그/박스줌·휠 줌 가능
|
|
fig.update_xaxes(
|
|
rangeslider_visible=False,
|
|
fixedrange=False,
|
|
row=1,
|
|
col=1,
|
|
)
|
|
fig.update_xaxes(fixedrange=False, row=2, col=1)
|
|
fig.update_yaxes(
|
|
title_text="가격 (KRW)",
|
|
fixedrange=False,
|
|
scaleanchor=None,
|
|
scaleratio=None,
|
|
row=1,
|
|
col=1,
|
|
)
|
|
fig.update_yaxes(
|
|
fixedrange=False,
|
|
scaleanchor=None,
|
|
scaleratio=None,
|
|
row=2,
|
|
col=1,
|
|
)
|
|
fig.show(
|
|
config={
|
|
"scrollZoom": True,
|
|
"displaylogo": False,
|
|
"doubleClick": "reset",
|
|
"modeBarButtonsToAdd": ["zoom2d", "pan2d", "resetScale2d"],
|
|
}
|
|
)
|
|
|
|
def load_all_frames(self) -> dict[int, pd.DataFrame]:
|
|
"""discovered 규칙용 전 간격 로드."""
|
|
from mtf_bb import load_frames_from_db
|
|
|
|
return load_frames_from_db(self.monitor, SYMBOL)
|
|
|
|
def _run_one_strategy(
|
|
self,
|
|
name: str,
|
|
df_1d: pd.DataFrame,
|
|
df_1h: pd.DataFrame,
|
|
df_3m: pd.DataFrame,
|
|
cfg: strategy.StrategyConfig,
|
|
frames: dict | None = None,
|
|
) -> tuple[pd.DataFrame, SimResult, int]:
|
|
"""한 전략으로 신호·백테스트. 반환: (df, result, 신호수)."""
|
|
df_sig = strategy.annotate_signals(
|
|
SYMBOL,
|
|
df_3m.copy(),
|
|
simulation=True,
|
|
df_1h=df_1h,
|
|
df_1d=df_1d,
|
|
config=cfg,
|
|
frames=frames,
|
|
)
|
|
n_sig = int((df_sig["point"] == 1).sum())
|
|
res = run_backtest(df_sig, df_1d, df_1h, config_name=name)
|
|
return df_sig, res, n_sig
|
|
|
|
def run(self, config: strategy.StrategyConfig | None = None) -> SimResult:
|
|
"""기본 BB vs 탐색 규칙 중 수익률·신호가 있는 쪽을 HTML에 표시."""
|
|
df_1d, df_1h, df_3m = self.load_mtf(SYMBOL)
|
|
trend = strategy.get_trend(df_1d, df_1h)
|
|
print(f"추세(최신): {trend}")
|
|
print(f"3분: {df_3m.index[0]} ~ {df_3m.index[-1]} ({len(df_3m)}봉)")
|
|
|
|
cfg_base = strategy.StrategyConfig(
|
|
name="01_기본_BB만",
|
|
use_discovered_rules=False,
|
|
use_regime_switch=False,
|
|
use_rsi_filter=False,
|
|
use_volume_filter=False,
|
|
use_squeeze_filter=False,
|
|
use_stop_loss=False,
|
|
)
|
|
df_base, res_base, n_base = self._run_one_strategy(
|
|
cfg_base.name, df_1d, df_1h, df_3m, cfg_base
|
|
)
|
|
print(f"\n[기본 BB] 신호 {n_base} | 수익 {res_base.total_return_pct:+.2f}% | 거래 {res_base.trade_count}")
|
|
|
|
candidates: list[tuple[str, pd.DataFrame, SimResult, int]] = [
|
|
(cfg_base.name, df_base, res_base, n_base),
|
|
]
|
|
|
|
try:
|
|
from rule_discovery import load_rules
|
|
|
|
rules = load_rules()
|
|
frames = self.load_all_frames()
|
|
if rules and frames:
|
|
cfg_disc = strategy.StrategyConfig(
|
|
name=rules.name,
|
|
use_discovered_rules=True,
|
|
use_regime_switch=False,
|
|
use_rsi_filter=False,
|
|
use_volume_filter=False,
|
|
use_squeeze_filter=False,
|
|
use_stop_loss=False,
|
|
)
|
|
df_disc, res_disc, n_disc = self._run_one_strategy(
|
|
cfg_disc.name, df_1d, df_1h, df_3m, cfg_disc, frames=frames
|
|
)
|
|
print(
|
|
f"[탐색 규칙] 신호 {n_disc} | 수익 {res_disc.total_return_pct:+.2f}% "
|
|
f"| 거래 {res_disc.trade_count}"
|
|
)
|
|
print(f" 매수: {rules.buy_all} | OR: {rules.buy_any}")
|
|
print(f" 매도: {rules.sell_all} | 손절: {rules.sell_stop}")
|
|
if n_disc > 0 and res_disc.trade_count > 0:
|
|
candidates.append((cfg_disc.name, df_disc, res_disc, n_disc))
|
|
except Exception as e:
|
|
print(f"[탐색 규칙] 스킵: {e}")
|
|
|
|
# 신호·거래 있는 후보 중 수익률 최대
|
|
valid = [c for c in candidates if c[3] > 0 and c[2].trade_count > 0]
|
|
if not valid:
|
|
valid = candidates
|
|
name, df_plot, result, n_sig = max(valid, key=lambda c: c[2].total_return_pct)
|
|
|
|
print(f"\n>>> HTML 적용: {name} (신호 {n_sig}, 거래 {result.trade_count}, {result.total_return_pct:+.2f}%)")
|
|
sigs = df_plot[df_plot["point"] == 1]
|
|
if len(sigs):
|
|
print(sigs["action"].value_counts().to_string())
|
|
|
|
print_backtest_report(result)
|
|
self.render_plotly(df_plot, trend, result)
|
|
return result
|
|
|
|
|
|
def run_mtf_analysis() -> None:
|
|
"""봉별 BB 백테스트 비교, 정책 저장, MTF 시뮬 차트."""
|
|
from mtf_bb import apply_policy, load_frames_from_db, run_interval_comparison, save_policy
|
|
|
|
monitor = Monitor()
|
|
policy, _ = run_interval_comparison(monitor)
|
|
save_policy(policy)
|
|
apply_policy(policy)
|
|
|
|
frames = load_frames_from_db(monitor, SYMBOL)
|
|
df_1d = frames.get(TREND_INTERVAL_1D)
|
|
if df_1d is None or df_1d.empty:
|
|
df_1d = frames[ENTRY_INTERVAL]
|
|
df_1h = frames.get(TREND_INTERVAL_1H)
|
|
if df_1h is None or df_1h.empty:
|
|
df_1h = frames[ENTRY_INTERVAL]
|
|
|
|
cfg = strategy.StrategyConfig(
|
|
name="MTF_BB",
|
|
use_mtf=True,
|
|
use_regime_switch=strategy.ACTIVE_CONFIG.use_regime_switch,
|
|
use_rsi_filter=False,
|
|
use_volume_filter=False,
|
|
use_squeeze_filter=False,
|
|
use_stop_loss=True,
|
|
)
|
|
df_sig = strategy.annotate_mtf_signals(SYMBOL, frames, df_1d, df_1h, policy, cfg)
|
|
trend = strategy.get_trend(df_1d, df_1h)
|
|
print(f"\nMTF 시뮬 ({policy.name}) | 추세: {trend}")
|
|
result = run_backtest(df_sig, df_1d, df_1h, config_name=policy.name)
|
|
print_backtest_report(result)
|
|
Simulation().render_plotly(df_sig, trend, result)
|
|
|
|
|
|
def run_discover() -> None:
|
|
"""모든 봉·캔들 특징으로 최적 규칙 탐색 후 JSON 저장."""
|
|
from rule_discovery import discover_rules, load_frames, save_rules
|
|
|
|
monitor = Monitor(cooldown_file=None)
|
|
frames = load_frames(monitor)
|
|
rules = discover_rules(frames)
|
|
save_rules(rules)
|
|
print(f"\n저장: discovered_rules.json")
|
|
print("HTML 차트: python simulation_1h.py")
|
|
|
|
|
|
def main() -> None:
|
|
sim = Simulation()
|
|
if len(sys.argv) > 1 and sys.argv[1] == "discover":
|
|
run_discover()
|
|
return
|
|
if len(sys.argv) > 1 and sys.argv[1] == "mtf":
|
|
run_mtf_analysis()
|
|
return
|
|
df_1d, df_1h, df_3m = sim.load_mtf(SYMBOL)
|
|
if len(sys.argv) > 1 and sys.argv[1] == "compare":
|
|
run_comparison(df_1d, df_1h, df_3m)
|
|
return
|
|
sim.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|