"""WF 윈도우 비교 시뮬레이션 v2 - 실거래 + 이후 시뮬 거래 통합. Phase 1: 실제 42건 거래를 WF 설정별로 허용/차단 재생 Phase 2: 마지막 실거래 이후 price_history 기반 신호로 추가 거래 시뮬 (추세 2h+5% + 15분 워치리스트, 모멘텀은 API 한계로 생략) → WF 상태는 Phase1에서 이어짐 비교 설정: A: WF=2 (min_wr=0.0, 즉 2연패시만 차단 — last2=[L,L]이면 차단) B: WF=3 (min_wr=0.34) C: WF=5 현행 (min_wr=0.40) D: WF 없음 """ import os as _os, sys as _sys _sys.path.insert(0, _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))) import os, time from datetime import datetime from dotenv import load_dotenv load_dotenv() import oracledb STOP_LOSS_PCT = float(os.getenv("STOP_LOSS_PCT", "1.5")) / 100 TIME_STOP_HOURS = int(os.getenv("TIME_STOP_HOURS", "8")) TIME_STOP_MIN_PCT = float(os.getenv("TIME_STOP_MIN_GAIN_PCT", "3")) / 100 TREND_MIN_PCT = 5.0 CONFIRM_MINUTES = 15 FEE = 0.0005 def get_conn(): return oracledb.connect( user=os.getenv('ORACLE_USER'), password=os.getenv('ORACLE_PASSWORD'), dsn=os.getenv('ORACLE_DSN'), config_dir=os.getenv('ORACLE_WALLET')) # ── WF 판단 ─────────────────────────────────────────── def is_wf_blocked(hist, window, min_wr): if window == 0: return False if len(hist) < window: return False wr = sum(hist[-window:]) / window return wr < min_wr # ── 추세 체크 (price_history 기반) ──────────────────── def check_trend(prices, idx): lb = 12 # 2h = 12 * 10분봉 if idx < lb: return False curr, past = prices[idx][0], prices[idx-lb][0] return past > 0 and (curr-past)/past*100 >= TREND_MIN_PCT # ── 포지션 시뮬 ─────────────────────────────────────── def simulate_pos(prices, buy_idx, buy_price): buy_dt = prices[buy_idx][1] peak = buy_price for price, ts in prices[buy_idx+1:]: if price > peak: peak = price elapsed_h = (ts - buy_dt).total_seconds() / 3600 pnl = (price - buy_price) / buy_price if (peak - price) / peak >= STOP_LOSS_PCT: net = (price*(1-FEE) - buy_price*(1+FEE)) / (buy_price*(1+FEE)) * 100 return net>0, price, ts, f"트레일링({pnl*100:+.1f}%)", net if elapsed_h >= TIME_STOP_HOURS and pnl < TIME_STOP_MIN_PCT: net = (price*(1-FEE) - buy_price*(1+FEE)) / (buy_price*(1+FEE)) * 100 return net>0, price, ts, "타임스탑", net lp, lt = prices[-1] net = (lp*(1-FEE) - buy_price*(1+FEE)) / (buy_price*(1+FEE)) * 100 return net>0, lp, lt, "데이터종료", net # ── Phase1: 실거래 재생 ─────────────────────────────── def phase1(real_trades, window, min_wr): """42건 실거래 재생. Returns (허용목록, 차단목록, history_per_ticker)""" history = {} allowed = [] blocked = [] for t in real_trades: ticker, is_win, pnl, profit, dt = t hist = history.get(ticker, []) if is_wf_blocked(hist, window, min_wr): blocked.append(('block', ticker, is_win, pnl, profit, dt)) else: allowed.append(('real', ticker, is_win, pnl, profit, dt)) hist = hist + [bool(is_win)] if window > 0 and len(hist) > window * 2: hist = hist[-window:] history[ticker] = hist return allowed, blocked, history # ── Phase2: price_history 신호 시뮬 ────────────────── def phase2(cur, history, real_last_dt, window, min_wr): """실거래 종료 이후 price_history 기반 신호 시뮬레이션.""" # 스캔 대상: 실거래에 등장한 종목 전체 tickers = list(history.keys()) if history else [] # 실거래 후 WF 해제 가능한 종목만 # (차단됐어도 shadow 없이는 해제 불가 → 차단 상태 종목 제외) active_tickers = [] for ticker in tickers: hist = history.get(ticker, []) if not is_wf_blocked(hist, window, min_wr): active_tickers.append(ticker) if not active_tickers: return [], history sim_trades = [] for ticker in active_tickers: cur.execute(""" SELECT price, recorded_at FROM price_history WHERE ticker=:t AND recorded_at > :dt ORDER BY recorded_at """, t=ticker, dt=real_last_dt) prices = cur.fetchall() if len(prices) < 13: continue hist = list(history.get(ticker, [])) watchlist_dt = None in_pos = False buy_idx = buy_price = None idx = 0 while idx < len(prices): price, dt = prices[idx] if in_pos: is_win, sp, sdt, reason, pnl = simulate_pos(prices, buy_idx, buy_price) next_idx = next((i for i,(_, ts) in enumerate(prices) if ts > sdt), len(prices)) profit = pnl * 3333333 / 100 # 포지션당 예산 기준 근사 sim_trades.append(('sim', ticker, is_win, pnl, profit, dt)) hist = hist + [bool(is_win)] if window > 0 and len(hist) > window * 2: hist = hist[-window:] history[ticker] = hist in_pos = False watchlist_dt = None idx = next_idx continue if is_wf_blocked(hist, window, min_wr): idx += 1 continue trend_ok = check_trend(prices, idx) if trend_ok: if watchlist_dt is None: watchlist_dt = dt elif (dt - watchlist_dt).total_seconds() >= CONFIRM_MINUTES * 60: in_pos = True buy_idx = idx buy_price = price watchlist_dt = None else: watchlist_dt = None idx += 1 return sim_trades, history # ── 요약 출력 ───────────────────────────────────────── def print_summary(label, p1_allowed, p1_blocked, p2_trades): all_trades = p1_allowed + p2_trades total = len(all_trades) wins = sum(1 for t in all_trades if t[2]) pnl = sum(t[4] for t in all_trades) wr = wins/total*100 if total else 0 blk = len(p1_blocked) p2_cnt = len(p2_trades) p2_win = sum(1 for t in p2_trades if t[2]) print(f"\n[{label}]") print(f" 실거래 허용: {len(p1_allowed)}건 | 차단: {blk}건") print(f" 추가 시뮬: {p2_cnt}건 ({p2_win}승)") print(f" ─────────────────────────────────────") print(f" 합계: {total}건 | 승률={wr:.1f}% | KRW={pnl:+,.0f}원") return {'label': label, 'total': total, 'wins': wins, 'wr': wr, 'pnl': pnl, 'blk': blk, 'p2': p2_cnt} def main(): conn = get_conn() cur = conn.cursor() cur.execute(""" SELECT ticker, is_win, pnl_pct, NVL(krw_profit,0), traded_at FROM trade_results ORDER BY traded_at """) real_trades = cur.fetchall() real_last_dt = real_trades[-1][4] print(f"실거래: {len(real_trades)}건 (마지막: {real_last_dt.strftime('%m-%d %H:%M')})") cur.execute("SELECT MAX(recorded_at) FROM price_history") ph_last = cur.fetchone()[0] print(f"price_history 끝: {ph_last.strftime('%m-%d %H:%M')}\n") configs = [ (2, 0.01, "WF=2 (2연패→차단)"), (3, 0.34, "WF=3"), (5, 0.40, "WF=5 현행"), (0, 0.00, "WF없음"), ] summary = [] for window, min_wr, label in configs: p1_allowed, p1_blocked, history = phase1(real_trades, window, min_wr) p2_trades, _ = phase2(cur, history, real_last_dt, window, min_wr) s = print_summary(label, p1_allowed, p1_blocked, p2_trades) summary.append(s) print(f"\n{'='*62}") print(f"{'설정':<22} {'허용':>5} {'차단':>5} {'추가시뮬':>8} {'승률':>7} {'KRW수익':>13}") print(f"{'─'*62}") for s in summary: print(f"{s['label']:<22} {s['total']-s['p2']:>5}건 {s['blk']:>5}건 " f"{s['p2']:>6}건 {s['wr']:>6.1f}% {s['pnl']:>+12,.0f}원") conn.close() if __name__ == "__main__": main()