chore: add WF/shadow/momentum analysis simulation scripts

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>
This commit is contained in:
joungmin
2026-03-01 23:58:42 +09:00
parent 29d48f0fe9
commit 54ce327c50
7 changed files with 1210 additions and 0 deletions

211
wf_cmp2.py Normal file
View File

@@ -0,0 +1,211 @@
"""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()