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:
211
wf_cmp2.py
Normal file
211
wf_cmp2.py
Normal 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()
|
||||
Reference in New Issue
Block a user