Scripts used to analyze and validate strategy changes: - wf_cmp.py: WF window size comparison on 42 real trades - wf_cmp2.py: WF comparison extended with price_history simulation - shadow_sim.py: shadow rehabilitation sim without strategy filters - shadow_sim2.py: post-rehabilitation performance simulation - shadow_sim3.py: shadow rehabilitation sim with full strategy filters - momentum_cmp.py: momentum filter A/B comparison - trend_check.py: 2h price gain distribution analysis per ticker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
212 lines
8.3 KiB
Python
212 lines
8.3 KiB
Python
"""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, 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()
|