refactor: MVC 구조 분리 + 미사용 파일 archive 정리
- tick_trader.py를 Controller로 축소, 로직을 3개 모듈로 분리: - core/signal.py: 시그널 감지, 지표 계산 (calc_vr, calc_atr, detect_signal) - core/order.py: Upbit 주문 실행 (매수/매도/취소/조회) - core/position_manager.py: 포지션 관리, DB sync, 복구, 청산 조건 - type hints, Google docstring, 구체적 예외 타입 적용 - 50줄 초과 함수 분리 (process_signal, restore_positions) - 미사용 파일 58개 archive/ 폴더로 이동 - README.md 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
158
archive/tests/shadow_sim2.py
Normal file
158
archive/tests/shadow_sim2.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Shadow 재활 이후 실제 진입 성과 시뮬레이션."""
|
||||
|
||||
import os as _os, sys as _sys
|
||||
_sys.path.insert(0, _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))))
|
||||
|
||||
import os
|
||||
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
|
||||
FEE = 0.0005
|
||||
REHABILITATE_WINS = 2
|
||||
|
||||
|
||||
def get_price_series(cur, ticker, from_dt):
|
||||
cur.execute("""
|
||||
SELECT price, recorded_at FROM price_history
|
||||
WHERE ticker = :t AND recorded_at >= :dt
|
||||
ORDER BY recorded_at
|
||||
""", t=ticker, dt=from_dt)
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def simulate_one(prices, buy_price, buy_dt):
|
||||
"""단일 포지션 시뮬레이션. Returns (is_win, sell_price, sell_dt, reason, pnl_pct)"""
|
||||
peak = buy_price
|
||||
for price, ts in prices:
|
||||
if price > peak:
|
||||
peak = price
|
||||
elapsed_h = (ts - buy_dt).total_seconds() / 3600
|
||||
pnl = (price - buy_price) / buy_price
|
||||
drop_from_peak = (peak - price) / peak
|
||||
if drop_from_peak >= STOP_LOSS_PCT:
|
||||
sell_pnl = (price*(1-FEE) - buy_price*(1+FEE)) / (buy_price*(1+FEE)) * 100
|
||||
return (sell_pnl > 0, price, ts, f"트레일링({pnl*100:+.1f}%)", sell_pnl)
|
||||
if elapsed_h >= TIME_STOP_HOURS and pnl < TIME_STOP_MIN_PCT:
|
||||
sell_pnl = (price*(1-FEE) - buy_price*(1+FEE)) / (buy_price*(1+FEE)) * 100
|
||||
return (sell_pnl > 0, price, ts, f"타임스탑({elapsed_h:.1f}h)", sell_pnl)
|
||||
last_price, last_ts = prices[-1]
|
||||
sell_pnl = (last_price*(1-FEE) - buy_price*(1+FEE)) / (buy_price*(1+FEE)) * 100
|
||||
return (sell_pnl > 0, last_price, last_ts, "데이터종료(보유중)", sell_pnl)
|
||||
|
||||
|
||||
def run_shadow_then_real(cur, ticker, wf_trigger_dt):
|
||||
"""shadow로 재활 후, 재활 시점 이후 실제 거래 성과 시뮬레이션."""
|
||||
prices = get_price_series(cur, ticker, wf_trigger_dt)
|
||||
if not prices:
|
||||
return None
|
||||
|
||||
# 1단계: shadow로 재활 시점 찾기
|
||||
shadow_wins = 0
|
||||
cursor_idx = 0
|
||||
rehab_dt = None
|
||||
|
||||
while cursor_idx < len(prices):
|
||||
buy_price, buy_dt = prices[cursor_idx]
|
||||
remaining = prices[cursor_idx + 1:]
|
||||
if not remaining:
|
||||
break
|
||||
|
||||
is_win, sell_price, sell_dt, reason, pnl = simulate_one(remaining, buy_price, buy_dt)
|
||||
if is_win:
|
||||
shadow_wins += 1
|
||||
else:
|
||||
shadow_wins = 0
|
||||
|
||||
if shadow_wins >= REHABILITATE_WINS:
|
||||
rehab_dt = sell_dt
|
||||
break
|
||||
|
||||
next_idx = next((i for i, (_, ts) in enumerate(prices) if ts > sell_dt), None)
|
||||
if next_idx is None:
|
||||
break
|
||||
cursor_idx = next_idx
|
||||
|
||||
if rehab_dt is None:
|
||||
return None # 재활 실패
|
||||
|
||||
# 2단계: 재활 이후 실제 거래 시뮬레이션
|
||||
print(f"\n ★ WF 해제 시점: {rehab_dt.strftime('%m-%d %H:%M')}")
|
||||
print(f" ─ 이후 실제 진입 시뮬레이션 ─")
|
||||
|
||||
post_prices = get_price_series(cur, ticker, rehab_dt)
|
||||
if not post_prices:
|
||||
print(" → 재활 이후 가격 데이터 없음")
|
||||
return
|
||||
|
||||
cursor_idx = 0
|
||||
trade_no = 0
|
||||
wins = 0
|
||||
total_pnl = 0.0
|
||||
|
||||
while cursor_idx < len(post_prices):
|
||||
buy_price, buy_dt = post_prices[cursor_idx]
|
||||
remaining = post_prices[cursor_idx + 1:]
|
||||
if not remaining:
|
||||
break
|
||||
|
||||
is_win, sell_price, sell_dt, reason, pnl = simulate_one(remaining, buy_price, buy_dt)
|
||||
trade_no += 1
|
||||
if is_win:
|
||||
wins += 1
|
||||
total_pnl += pnl
|
||||
|
||||
mark = "✅" if is_win else "❌"
|
||||
print(f" 실제#{trade_no}: {buy_price:.4f}→{sell_price:.4f}원 | {mark} {pnl:+.2f}% | {reason} ({sell_dt.strftime('%m-%d %H:%M')})")
|
||||
|
||||
next_idx = next((i for i, (_, ts) in enumerate(post_prices) if ts > sell_dt), None)
|
||||
if next_idx is None:
|
||||
break
|
||||
cursor_idx = next_idx
|
||||
|
||||
if trade_no > 0:
|
||||
wr = wins / trade_no * 100
|
||||
print(f"\n 📊 재활 후 성과: {trade_no}건 중 {wins}승 | 승률={wr:.0f}% | 누적PnL={total_pnl:+.2f}%")
|
||||
return {'trades': trade_no, 'wins': wins, 'win_rate': wr, 'total_pnl': total_pnl}
|
||||
return None
|
||||
|
||||
|
||||
def run():
|
||||
conn = oracledb.connect(
|
||||
user=os.getenv('ORACLE_USER'),
|
||||
password=os.getenv('ORACLE_PASSWORD'),
|
||||
dsn=os.getenv('ORACLE_DSN'),
|
||||
config_dir=os.getenv('ORACLE_WALLET')
|
||||
)
|
||||
cur = conn.cursor()
|
||||
|
||||
results = {}
|
||||
for ticker in ['KRW-DKA', 'KRW-LAYER', 'KRW-SIGN']:
|
||||
cur.execute("""
|
||||
SELECT traded_at FROM trade_results
|
||||
WHERE ticker = :t ORDER BY traded_at
|
||||
""", t=ticker)
|
||||
rows = cur.fetchall()
|
||||
wf_trigger_dt = rows[4][0]
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"[{ticker}] WF차단 발동: {wf_trigger_dt.strftime('%m-%d %H:%M')}")
|
||||
r = run_shadow_then_real(cur, ticker, wf_trigger_dt)
|
||||
if r:
|
||||
results[ticker] = r
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("전체 요약")
|
||||
print(f"{'='*60}")
|
||||
for ticker, r in results.items():
|
||||
print(f"{ticker}: 재활 후 {r['trades']}건 | 승률={r['win_rate']:.0f}% | 누적={r['total_pnl']:+.2f}%")
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
Reference in New Issue
Block a user