WLD 전용 BB MTF 전략 및 HTML 시뮬 최적화
- 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>
This commit is contained in:
962
simulation_1h.py
962
simulation_1h.py
@@ -1,333 +1,671 @@
|
||||
"""
|
||||
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 yfinance as yf
|
||||
import plotly.graph_objs as go
|
||||
from plotly import subplots
|
||||
import plotly.io as pio
|
||||
from datetime import datetime
|
||||
pio.renderers.default = 'browser'
|
||||
from plotly import subplots
|
||||
|
||||
from config import *
|
||||
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 render_plotly(self, symbol: str, interval_minutes: int, data: pd.DataFrame, inverseData: pd.DataFrame) -> None:
|
||||
fig = subplots.make_subplots(
|
||||
rows=3, cols=1,
|
||||
subplot_titles=("캔들", "이격도/거래량", "장기 이격도"),
|
||||
shared_xaxes=True, horizontal_spacing=0.03, vertical_spacing=0.03,
|
||||
row_heights=[0.6, 0.2, 0.2]
|
||||
)
|
||||
|
||||
# Row 1: 캔들 + 이동평균 + 볼린저
|
||||
fig.add_trace(go.Candlestick(x=data.index, open=data['Open'], high=data['High'], low=data['Low'], close=data['Close'], name='캔들'), row=1, col=1)
|
||||
for ma_col, color in [('MA5','red'),('MA20','blue'),('MA40','green'),('MA120','purple'),('MA200','brown'),('MA240','darkred'),('MA720','cyan'),('MA1440','magenta')]:
|
||||
if ma_col in data.columns:
|
||||
fig.add_trace(go.Scatter(x=data.index, y=data[ma_col], name=ma_col, mode='lines', line=dict(color=color, width=1)), row=1, col=1)
|
||||
if 'Lower' in data.columns and 'Upper' in data.columns:
|
||||
fig.add_trace(go.Scatter(x=data.index, y=data['Lower'], name='볼린저 하단', mode='lines', line=dict(color='grey', width=1, dash='dot')), row=1, col=1)
|
||||
fig.add_trace(go.Scatter(x=data.index, y=data['Upper'], name='볼린저 상단', mode='lines', line=dict(color='grey', width=1, dash='dot')), row=1, col=1)
|
||||
|
||||
# 매수 포인트
|
||||
for sig, color in [('movingaverage','red'),('deviation40','orange'),('Deviation720','blue'),('deviation1440','purple'),('fall_6p','black')]:
|
||||
pts = data[(data['point']==1) & (data['signal']==sig)]
|
||||
if len(pts)>0:
|
||||
fig.add_trace(go.Scatter(x=pts.index, y=pts['Close'], mode='markers', name=f'{sig} 매수', marker=dict(color=color, size=8, symbol='circle')), row=1, col=1)
|
||||
|
||||
# 매도 포인트: inverseData의 buy 신호 중 fall_6p, deviation40만 일반 그래프 가격축에 매도로 표시
|
||||
inv_sell_pts = inverseData[(inverseData['point']==1) & (inverseData['signal'].isin(['deviation40','fall_6p']))]
|
||||
if len(inv_sell_pts)>0:
|
||||
idx = inv_sell_pts.index.intersection(data.index)
|
||||
if len(idx)>0:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=idx,
|
||||
y=data.loc[idx, 'Close'],
|
||||
mode='markers',
|
||||
name='매도',
|
||||
marker=dict(color='orange', size=10, symbol='triangle-down')
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
# Row 2: 이격도 + 거래량
|
||||
for dev_col, color, width in [('Deviation5','red',1),('Deviation20','blue',1),('Deviation40','green',2),('Deviation120','purple',1),('Deviation200','brown',1),('Deviation720','darkred',2),('Deviation720','cyan',1),('Deviation1440','magenta',1)]:
|
||||
if dev_col in data.columns:
|
||||
fig.add_trace(go.Scatter(x=data.index, y=data[dev_col], name=dev_col, mode='lines', line=dict(color=color, width=width)), row=2, col=1)
|
||||
if 'Volume' in data.columns:
|
||||
fig.add_trace(go.Bar(x=data.index, y=data['Volume'], name='거래량', marker_color='lightgray', opacity=0.5), row=2, col=1)
|
||||
|
||||
# Row 3: 장기 이격도 및 기준선
|
||||
for dev_col, color in [('Deviation720','darkred'),('Deviation1440','magenta')]:
|
||||
if dev_col in data.columns:
|
||||
fig.add_trace(go.Scatter(x=data.index, y=data[dev_col], name=f'{dev_col}(장기)', mode='lines', line=dict(color=color, width=2)), row=3, col=1)
|
||||
for h, color in [(90,'red'),(95,'green'),(100,'black')]:
|
||||
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||
|
||||
# ----------------- 인버스용 트레이스 (초기 숨김) -----------------
|
||||
n_orig = len(fig.data)
|
||||
|
||||
# Row 1: 캔들/MA/볼린저 (inverseData)
|
||||
fig.add_trace(go.Candlestick(x=inverseData.index, open=inverseData['Open'], high=inverseData['High'], low=inverseData['Low'], close=inverseData['Close'], name='캔들(인버스)', showlegend=True, visible=False), row=1, col=1)
|
||||
for ma_col, color in [('MA5','red'),('MA20','blue'),('MA40','green'),('MA120','purple'),('MA200','brown'),('MA240','darkred'),('MA720','cyan'),('MA1440','magenta')]:
|
||||
if ma_col in inverseData.columns:
|
||||
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData[ma_col], name=f'{ma_col}(인버스)', mode='lines', line=dict(color=color, width=1), showlegend=True, visible=False), row=1, col=1)
|
||||
if 'Lower' in inverseData.columns and 'Upper' in inverseData.columns:
|
||||
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData['Lower'], name='볼린저 하단(인버스)', mode='lines', line=dict(color='grey', width=1, dash='dot'), showlegend=True, visible=False), row=1, col=1)
|
||||
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData['Upper'], name='볼린저 상단(인버스)', mode='lines', line=dict(color='grey', width=1, dash='dot'), showlegend=True, visible=False), row=1, col=1)
|
||||
|
||||
# 인버스 매수 포인트: fall_6p, deviation40만 표시
|
||||
for sig, color in [('deviation40','orange'),('fall_6p','black')]:
|
||||
pts_inv = inverseData[(inverseData['point']==1) & (inverseData['signal']==sig)]
|
||||
if len(pts_inv)>0:
|
||||
fig.add_trace(go.Scatter(x=pts_inv.index, y=inverseData.loc[pts_inv.index,'Close'], mode='markers', name=f'{sig} 매수(인버스)', marker=dict(color=color, size=8, symbol='circle'), showlegend=True, visible=False), row=1, col=1)
|
||||
|
||||
# 인버스 보기에서의 매도 포인트: 일반 그래프의 매수를 인버스 그래프의 매도로 표시 (모든 매수 신호 반영)
|
||||
normal_to_inv_sell = data[(data['point']==1)]
|
||||
if len(normal_to_inv_sell) > 0:
|
||||
idx2 = normal_to_inv_sell.index.intersection(inverseData.index)
|
||||
if len(idx2) > 0:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=idx2,
|
||||
y=inverseData.loc[idx2, 'Close'],
|
||||
mode='markers',
|
||||
name='매도(일반→인버스)',
|
||||
marker=dict(color='orange', size=10, symbol='triangle-down'),
|
||||
showlegend=True,
|
||||
visible=False
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
# Row 2: 이격도 + 거래량 (inverseData)
|
||||
for dev_col, color, width in [('Deviation5','red',1),('Deviation20','blue',1),('Deviation40','green',2),('Deviation120','purple',1),('Deviation200','brown',1),('Deviation720','darkred',2),('Deviation720','cyan',1),('Deviation1440','magenta',1)]:
|
||||
if dev_col in inverseData.columns:
|
||||
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData[dev_col], name=f'{dev_col}(인버스)', mode='lines', line=dict(color=color, width=width), showlegend=True, visible=False), row=2, col=1)
|
||||
if 'Volume' in inverseData.columns:
|
||||
fig.add_trace(go.Bar(x=inverseData.index, y=inverseData['Volume'], name='거래량(인버스)', marker_color='lightgray', opacity=0.5, showlegend=True, visible=False), row=2, col=1)
|
||||
|
||||
# Row 3: 장기 이격도 (inverseData)
|
||||
for dev_col, color in [('Deviation720','darkred'),('Deviation1440','magenta')]:
|
||||
if dev_col in inverseData.columns:
|
||||
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData[dev_col], name=f'{dev_col}(장기-인버스)', mode='lines', line=dict(color=color, width=2), showlegend=True, visible=False), row=3, col=1)
|
||||
|
||||
n_total = len(fig.data)
|
||||
n_inv = n_total - n_orig
|
||||
visible_orig = [True]*n_orig + [False]*n_inv
|
||||
visible_inv = [False]*n_orig + [True]*n_inv
|
||||
legendtitle_orig = {'text': '일반 그래프'}
|
||||
legendtitle_inv = {'text': '인버스 그래프'}
|
||||
|
||||
fig.update_layout(
|
||||
height=1000,
|
||||
margin=dict(t=180, l=40, r=240, b=40),
|
||||
title=dict(
|
||||
text=f"{symbol}, {interval_minutes} 분봉, ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})",
|
||||
x=0.5,
|
||||
xanchor='center',
|
||||
y=0.995,
|
||||
yanchor='top',
|
||||
pad=dict(t=10, b=12)
|
||||
),
|
||||
xaxis_rangeslider_visible=False,
|
||||
xaxis1_rangeslider_visible=False,
|
||||
xaxis2_rangeslider_visible=False,
|
||||
legend=dict(orientation='v', yref='paper', yanchor='top', y=1.0, xref='paper', xanchor='left', x=1.02, title=legendtitle_orig),
|
||||
dragmode='zoom',
|
||||
updatemenus=[dict(
|
||||
type='buttons',
|
||||
direction='left',
|
||||
x=0.0,
|
||||
xanchor='left',
|
||||
y=1.11,
|
||||
yanchor='top',
|
||||
pad=dict(t=0, r=10, b=0, l=0),
|
||||
buttons=[
|
||||
dict(
|
||||
label='홈',
|
||||
method='update',
|
||||
args=[
|
||||
{'visible': visible_orig},
|
||||
{
|
||||
'legend': {'title': legendtitle_orig},
|
||||
'xaxis.autorange': True,
|
||||
'xaxis2.autorange': True,
|
||||
'xaxis3.autorange': True,
|
||||
'yaxis.autorange': True,
|
||||
'yaxis2.autorange': True,
|
||||
'yaxis3.autorange': True,
|
||||
}
|
||||
],
|
||||
execute=True
|
||||
),
|
||||
dict(
|
||||
label='인버스',
|
||||
method='update',
|
||||
args=[
|
||||
{'visible': visible_inv},
|
||||
{'legend': {'title': legendtitle_inv, 'orientation': 'v', 'y': 1.0, 'yanchor': 'top', 'x': 1.02, 'xanchor': 'left'}}
|
||||
],
|
||||
args2=[
|
||||
{'visible': visible_orig},
|
||||
{'legend': {'title': legendtitle_orig, 'orientation': 'v', 'y': 1.0, 'yanchor': 'top', 'x': 1.02, 'xanchor': 'left'}}
|
||||
],
|
||||
execute=True
|
||||
),
|
||||
]
|
||||
)]
|
||||
)
|
||||
fig.update_xaxes(title_text='시간', row=3, col=1)
|
||||
fig.update_yaxes(title_text='가격 (KRW)', row=1, col=1)
|
||||
fig.update_yaxes(title_text='이격도/거래량', row=2, col=1)
|
||||
fig.update_yaxes(title_text='장기 이격도', row=3, col=1)
|
||||
|
||||
fig.show(config={'scrollZoom': True, 'displaylogo': False})
|
||||
def __init__(self) -> None:
|
||||
self.monitor = Monitor()
|
||||
self.INTERVAL_MAP = {
|
||||
60: "60m",
|
||||
240: "4h",
|
||||
}
|
||||
self.monitor = Monitor(cooldown_file=None)
|
||||
|
||||
def detect_turnaround_signal(self, symbol, data, interval=0, params=None):
|
||||
if len(data) < 7:
|
||||
return None
|
||||
current_data = data.iloc[-1]
|
||||
if current_data.get('point', 0) == 1:
|
||||
return {
|
||||
'alert': True,
|
||||
'details': f"매수신호: {current_data.get('signal', 'unknown')}"
|
||||
}
|
||||
return {'alert': False, 'details': "매수신호 없음"}
|
||||
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)
|
||||
|
||||
def fetch_price_history(self, symbol: str, interval_minutes: int, days: int = 30) -> pd.DataFrame:
|
||||
if symbol in KR_COINS:
|
||||
bong_count = 3000
|
||||
return self.monitor.get_coin_more_data(symbol, interval_minutes, bong_count=bong_count)
|
||||
if interval_minutes not in self.INTERVAL_MAP:
|
||||
raise ValueError("interval must be 60 or 240")
|
||||
interval_str = self.INTERVAL_MAP[interval_minutes]
|
||||
df = yf.download(
|
||||
tickers=symbol,
|
||||
period=f"{days}d",
|
||||
interval=interval_str,
|
||||
progress=False,
|
||||
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}"
|
||||
)
|
||||
if df.empty:
|
||||
raise RuntimeError("No data fetched. Check symbol or interval support.")
|
||||
return df
|
||||
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,
|
||||
)
|
||||
|
||||
def analyze_bottom_period(self, symbol: str, interval_minutes: int, days: int = 90):
|
||||
data = self.fetch_price_history(symbol, interval_minutes, days)
|
||||
data = self.monitor.calculate_technical_indicators(data)
|
||||
data = self.monitor.annotate_signals(symbol, data, simulation=True)
|
||||
print(f"데이터 기간: {data.index[0]} ~ {data.index[-1]}")
|
||||
print(f"총 데이터 수: {len(data)}")
|
||||
bottom_start = pd.Timestamp('2025-06-22')
|
||||
bottom_end = pd.Timestamp('2025-07-09')
|
||||
bottom_data = data[(data.index >= bottom_start) & (data.index <= bottom_end)]
|
||||
if len(bottom_data) == 0:
|
||||
print("저점 기간 데이터가 없습니다.")
|
||||
return None, []
|
||||
print(f"\n저점 기간 데이터: {bottom_data.index[0]} ~ {bottom_data.index[-1]}")
|
||||
print(f"저점 기간 데이터 수: {len(bottom_data)}")
|
||||
print("\n=== 저점 기간 기술적 지표 분석 ===")
|
||||
min_price = bottom_data['Low'].min()
|
||||
max_price = bottom_data['High'].max()
|
||||
avg_price = bottom_data['Close'].mean()
|
||||
print(f"최저가: {min_price:.4f}")
|
||||
print(f"최고가: {max_price:.4f}")
|
||||
print(f"평균가: {avg_price:.4f}")
|
||||
print(f"가격 변동폭: {((max_price - min_price) / min_price * 100):.2f}%")
|
||||
bb_lower_min = bottom_data['Lower'].min()
|
||||
bb_upper_max = bottom_data['Upper'].max()
|
||||
print(f"\n볼린저 밴드 분석:")
|
||||
print(f"하단 밴드 최저: {bb_lower_min:.4f}")
|
||||
print(f"상단 밴드 최고: {bb_upper_max:.4f}")
|
||||
volume_avg = bottom_data['Volume'].mean()
|
||||
volume_max = bottom_data['Volume'].max()
|
||||
print(f"\n거래량 분석:")
|
||||
print(f"평균 거래량: {volume_avg:.0f}")
|
||||
print(f"최대 거래량: {volume_max:.0f}")
|
||||
actual_bottom_idx = bottom_data['Low'].idxmin()
|
||||
actual_bottom_price = bottom_data.loc[actual_bottom_idx, 'Low']
|
||||
actual_bottom_date = actual_bottom_idx
|
||||
print(f"\n실제 저점:")
|
||||
print(f"날짜: {actual_bottom_date}")
|
||||
print(f"가격: {actual_bottom_price:.4f}")
|
||||
print(f"볼린저 하단 대비: {((actual_bottom_price - bottom_data.loc[actual_bottom_idx, 'Lower']) / bottom_data.loc[actual_bottom_idx, 'Lower'] * 100):.2f}%")
|
||||
print(f"\n=== 매수 신호 분석 ===")
|
||||
bottom_alerts = bottom_data[bottom_data['point'] == 1]
|
||||
alerts = [(idx, row['Close']) for idx, row in bottom_alerts.iterrows()]
|
||||
print(f"저점 기간 매수 신호 수: {len(alerts)}")
|
||||
if alerts:
|
||||
print("매수 신호 발생 시점:")
|
||||
for date, price in alerts:
|
||||
print(f" {date}: {price:.4f}")
|
||||
return bottom_data, alerts
|
||||
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 run_simulation(self, symbol: str, interval_minutes: int, days: int = 30):
|
||||
data = self.fetch_price_history(symbol, interval_minutes)
|
||||
def load_all_frames(self) -> dict[int, pd.DataFrame]:
|
||||
"""discovered 규칙용 전 간격 로드."""
|
||||
from mtf_bb import load_frames_from_db
|
||||
|
||||
inverseData = self.monitor.inverse_data(data)
|
||||
inverseData = self.monitor.annotate_signals(symbol, inverseData, simulation=True)
|
||||
return load_frames_from_db(self.monitor, SYMBOL)
|
||||
|
||||
data = self.monitor.calculate_technical_indicators(data)
|
||||
data = self.monitor.annotate_signals(symbol, data, simulation=True)
|
||||
print(f"데이터 기간: {data.index[0]} ~ {data.index[-1]}")
|
||||
print(f"총 데이터 수: {len(data)}")
|
||||
alerts = []
|
||||
for i in range(len(data)):
|
||||
if data['point'].iloc[i] == 1:
|
||||
alerts.append((data.index[i], data['Close'].iloc[i]))
|
||||
print(f"\n총 매수 신호 수: {len(alerts)}")
|
||||
ma_signals = len(data[(data['point'] == 1) & (data['signal'] == 'movingaverage')])
|
||||
dev40_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation40')])
|
||||
dev240_signals = len(data[(data['point'] == 1) & (data['signal'] == 'Deviation720')])
|
||||
dev1440_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation1440')])
|
||||
print(f" - MA 신호: {ma_signals}")
|
||||
print(f" - Dev40 신호: {dev40_signals}")
|
||||
print(f" - Dev240 신호: {dev240_signals}")
|
||||
print(f" - Dev1440 신호: {dev1440_signals}")
|
||||
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
|
||||
|
||||
# Plotly 기반 시각화로 전환
|
||||
self.render_plotly(symbol, interval_minutes, data, inverseData)
|
||||
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__":
|
||||
sim = Simulation()
|
||||
interval = 60
|
||||
days = 90
|
||||
target_coins = KR_COINS
|
||||
#target_coins = ['XRP']
|
||||
show_graphs = True
|
||||
for symbol in target_coins:
|
||||
print(f"\n=== {symbol} 저점 기간 분석 시작 ===")
|
||||
try:
|
||||
bottom_data, alerts = sim.analyze_bottom_period(symbol, interval, days)
|
||||
print(f"\n=== {symbol} 전체 기간 시뮬레이션 ===")
|
||||
if show_graphs:
|
||||
sim.run_simulation(symbol, interval, days)
|
||||
else:
|
||||
data = sim.fetch_price_history(symbol, interval, days)
|
||||
|
||||
inverseData = sim.monitor.inverse_data(data)
|
||||
inverseData = sim.monitor.annotate_signals(symbol, inverseData, simulation=True)
|
||||
|
||||
data = sim.monitor.calculate_technical_indicators(data)
|
||||
data = sim.monitor.annotate_signals(symbol, data, simulation=True)
|
||||
|
||||
total_signals = len(data[data['point'] == 1])
|
||||
ma_signals = len(data[(data['point'] == 1) & (data['signal'] == 'movingaverage')])
|
||||
dev40_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation40')])
|
||||
dev240_signals = len(data[(data['point'] == 1) & (data['signal'] == 'Deviation720')])
|
||||
dev1440_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation1440')])
|
||||
print(f"총 매수 신호: {total_signals}")
|
||||
print(f" - MA 신호: {ma_signals}")
|
||||
print(f" - Dev40 신호: {dev40_signals}")
|
||||
print(f" - Dev240 신호: {dev240_signals}")
|
||||
print(f" - Dev1440 신호: {dev1440_signals}")
|
||||
except Exception as e:
|
||||
print(f"Error analyzing {symbol}: {str(e)}")
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user