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:
308
archive/tests/sim10m.py
Normal file
308
archive/tests/sim10m.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""sim10m.py — 10분봉 + 극단적 거래량 즉시 진입 전략 시뮬.
|
||||
|
||||
기존 전략 (1h봉 vol-lead) vs 신규 전략 (10분봉 + 100x 이상 거래량 즉시 진입) 비교.
|
||||
데이터: 최근 45일 Upbit API
|
||||
"""
|
||||
|
||||
import pickle
|
||||
import time
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pandas as pd
|
||||
import pyupbit
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(dotenv_path=Path(__file__).parent / ".env")
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
# ── 파라미터 ──────────────────────────────────────────────
|
||||
SIM_DAYS = 45
|
||||
TOP30_FILE = Path("top30_tickers.pkl")
|
||||
CACHE_FILE = Path("sim10m_cache.pkl")
|
||||
TOP_N = 20
|
||||
|
||||
FEE = 0.0005
|
||||
TIME_STOP_MIN_PCT = 3.0
|
||||
ATR_MULT = 1.5
|
||||
ATR_MIN = 0.010
|
||||
ATR_MAX = 0.020
|
||||
|
||||
# A: 기존 1h봉
|
||||
A_LOCAL_VOL = 5 # 봉수 (= 5h)
|
||||
A_VOL_MULT = 2.0
|
||||
A_QUIET_PCT = 2.0
|
||||
A_THRESH = 4.8
|
||||
A_SIGNAL_TO = 8 # 신호 유효 봉수 (= 8h)
|
||||
A_ATR_CANDLES = 5
|
||||
A_TIME_STOP = 8 # 타임스탑 봉수 (= 8h)
|
||||
|
||||
# B: 신규 10분봉
|
||||
B_LOCAL_VOL = 30 # 봉수 (5h = 30 × 10min)
|
||||
B_VOL_MULT = 2.0
|
||||
B_EXTREME_VOL = 100 # 이 이상 → 횡보 조건 면제, 다음 봉 즉시 진입
|
||||
B_QUIET_CANDLES = 12 # 2h = 12봉
|
||||
B_QUIET_PCT = 2.0
|
||||
B_THRESH = 4.8
|
||||
B_SIGNAL_TO = 48 # 신호 유효 봉수 (8h = 48봉)
|
||||
B_ATR_CANDLES = 30 # ATR 봉수 (5h = 30봉)
|
||||
B_TIME_STOP = 48 # 타임스탑 봉수 (8h = 48봉)
|
||||
|
||||
|
||||
# ── 데이터 로드 ──────────────────────────────────────────
|
||||
def fetch_ohlcv(ticker, interval, days):
|
||||
"""OHLCV 과거 데이터 페이징 로드."""
|
||||
target_start = datetime.now() - timedelta(days=days)
|
||||
all_dfs, to, prev_oldest = [], None, None
|
||||
while True:
|
||||
kwargs = dict(ticker=ticker, interval=interval, count=200)
|
||||
if to:
|
||||
kwargs["to"] = to
|
||||
df = pyupbit.get_ohlcv(**kwargs)
|
||||
if df is None or df.empty:
|
||||
break
|
||||
df.index = df.index.tz_localize(None)
|
||||
oldest = df.index[0]
|
||||
# 더 이상 과거로 못 가면 중단 (상장일에 도달)
|
||||
if prev_oldest is not None and oldest >= prev_oldest:
|
||||
all_dfs.append(df)
|
||||
break
|
||||
all_dfs.append(df)
|
||||
prev_oldest = oldest
|
||||
if oldest <= target_start:
|
||||
break
|
||||
to = oldest.strftime("%Y-%m-%d %H:%M:%S")
|
||||
time.sleep(0.15)
|
||||
if not all_dfs:
|
||||
return None
|
||||
result = pd.concat(all_dfs).sort_index().drop_duplicates()
|
||||
return result[result.index >= target_start]
|
||||
|
||||
|
||||
def load_data(tickers):
|
||||
if CACHE_FILE.exists():
|
||||
print(f"캐시 로드: {CACHE_FILE}")
|
||||
return pickle.load(open(CACHE_FILE, "rb"))
|
||||
|
||||
data = {"1h": {}, "10m": {}}
|
||||
for idx, ticker in enumerate(tickers, 1):
|
||||
print(f" [{idx}/{len(tickers)}] {ticker}...", end=" ", flush=True)
|
||||
df1h = fetch_ohlcv(ticker, "minute60", SIM_DAYS)
|
||||
df10m = fetch_ohlcv(ticker, "minute10", SIM_DAYS)
|
||||
n1h = len(df1h) if df1h is not None else 0
|
||||
n10m = len(df10m) if df10m is not None else 0
|
||||
print(f"1h={n1h}봉 10m={n10m}봉")
|
||||
if df1h is not None and n1h >= 50: data["1h"][ticker] = df1h
|
||||
if df10m is not None and n10m >= 200: data["10m"][ticker] = df10m
|
||||
|
||||
pickle.dump(data, open(CACHE_FILE, "wb"))
|
||||
print(f"캐시 저장: {CACHE_FILE}\n")
|
||||
return data
|
||||
|
||||
|
||||
# ── ATR 계산 (시뮬용) ─────────────────────────────────────
|
||||
def calc_atr(df, buy_idx, n):
|
||||
sub = df.iloc[max(0, buy_idx - n - 1):buy_idx]
|
||||
if len(sub) < 3:
|
||||
return ATR_MIN
|
||||
try:
|
||||
avg = ((sub["high"] - sub["low"]) / sub["low"]).iloc[-n:].mean()
|
||||
return float(max(ATR_MIN, min(ATR_MAX, avg * ATR_MULT)))
|
||||
except Exception:
|
||||
return ATR_MIN
|
||||
|
||||
|
||||
# ── 포지션 시뮬 ──────────────────────────────────────────
|
||||
def simulate_pos(df, buy_idx, buy_price, stop_pct, time_stop_candles):
|
||||
peak = buy_price
|
||||
for i in range(buy_idx + 1, len(df)):
|
||||
row = df.iloc[i]
|
||||
ts = df.index[i]
|
||||
if row["high"] > peak:
|
||||
peak = row["high"]
|
||||
if row["low"] <= peak * (1 - stop_pct):
|
||||
sp = peak * (1 - stop_pct)
|
||||
pnl = (sp * (1-FEE) - buy_price * (1+FEE)) / (buy_price * (1+FEE)) * 100
|
||||
return pnl > 0, sp, ts, f"트레일링({pnl:+.1f}%)", pnl
|
||||
pnl_now = (row["close"] - buy_price) / buy_price * 100
|
||||
if (i - buy_idx) >= time_stop_candles and pnl_now < TIME_STOP_MIN_PCT:
|
||||
pnl = (row["close"] * (1-FEE) - buy_price * (1+FEE)) / (buy_price * (1+FEE)) * 100
|
||||
return pnl > 0, row["close"], ts, "타임스탑", pnl
|
||||
last = df.iloc[-1]["close"]
|
||||
pnl = (last * (1-FEE) - buy_price * (1+FEE)) / (buy_price * (1+FEE)) * 100
|
||||
return pnl > 0, last, df.index[-1], "데이터종료", pnl
|
||||
|
||||
|
||||
# ── 전략 A: 기존 1h봉 vol-lead ──────────────────────────
|
||||
def run_a(df):
|
||||
trades = []
|
||||
sig_i = sig_p = None
|
||||
in_pos = False
|
||||
buy_idx = buy_price = stop_pct = None
|
||||
i = max(A_LOCAL_VOL + 2, 3)
|
||||
|
||||
while i < len(df):
|
||||
if in_pos:
|
||||
is_win, sp, sdt, reason, pnl = simulate_pos(df, buy_idx, buy_price, stop_pct, A_TIME_STOP)
|
||||
next_i = next((j for j in range(i, len(df)) if df.index[j] > sdt), len(df))
|
||||
trades.append((is_win, pnl, df.index[buy_idx], sdt, "일반"))
|
||||
in_pos = False
|
||||
sig_i = sig_p = None
|
||||
i = next_i
|
||||
continue
|
||||
|
||||
close = df.iloc[i]["close"]
|
||||
chg_2h = abs(close - df.iloc[i-2]["close"]) / df.iloc[i-2]["close"] * 100
|
||||
quiet = chg_2h < A_QUIET_PCT
|
||||
vol_p = df.iloc[i-1]["volume"]
|
||||
vol_avg = df.iloc[i-A_LOCAL_VOL-1:i-1]["volume"].mean()
|
||||
spike = vol_avg > 0 and vol_p >= vol_avg * A_VOL_MULT
|
||||
|
||||
if quiet and spike:
|
||||
if sig_i is None: sig_i, sig_p = i, close
|
||||
else:
|
||||
if sig_i is not None and close < sig_p: sig_i = sig_p = None
|
||||
if sig_i is not None and (i - sig_i) > A_SIGNAL_TO:
|
||||
sig_i = sig_p = None
|
||||
if sig_i is not None and (close - sig_p) / sig_p * 100 >= A_THRESH:
|
||||
in_pos = True; buy_idx = i; buy_price = close
|
||||
stop_pct = calc_atr(df, i, A_ATR_CANDLES)
|
||||
sig_i = sig_p = None
|
||||
i += 1
|
||||
return trades
|
||||
|
||||
|
||||
# ── 전략 B: 10분봉 + 극단적 거래량 즉시 진입 ────────────
|
||||
def run_b(df):
|
||||
# trade tuple: (is_win, pnl, buy_dt, sell_dt, entry_type)
|
||||
# entry_type: '일반' | '극단'
|
||||
trades = []
|
||||
sig_i = sig_p = None
|
||||
extreme_pending = False
|
||||
in_pos = False
|
||||
buy_idx = buy_price = stop_pct = None
|
||||
i = max(B_LOCAL_VOL + 2, B_QUIET_CANDLES + 1)
|
||||
|
||||
while i < len(df):
|
||||
if in_pos:
|
||||
is_win, sp, sdt, reason, pnl = simulate_pos(df, buy_idx, buy_price, stop_pct, B_TIME_STOP)
|
||||
next_i = next((j for j in range(i, len(df)) if df.index[j] > sdt), len(df))
|
||||
trades.append((is_win, pnl, df.index[buy_idx], sdt, entry_type))
|
||||
in_pos = False
|
||||
sig_i = sig_p = None
|
||||
extreme_pending = False
|
||||
i = next_i
|
||||
continue
|
||||
|
||||
close = df.iloc[i]["close"]
|
||||
vol_p = df.iloc[i-1]["volume"]
|
||||
vol_avg = df.iloc[i-B_LOCAL_VOL-1:i-1]["volume"].mean()
|
||||
vol_r = vol_p / vol_avg if vol_avg > 0 else 0
|
||||
|
||||
# 극단적 거래량 → 다음 봉 진입 대기 설정
|
||||
if not extreme_pending and vol_r >= B_EXTREME_VOL:
|
||||
extreme_pending = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 극단적 거래량 다음 봉 → 즉시 진입
|
||||
if extreme_pending:
|
||||
in_pos = True; buy_idx = i; buy_price = close
|
||||
stop_pct = calc_atr(df, i, B_ATR_CANDLES)
|
||||
entry_type = "극단"
|
||||
extreme_pending = False
|
||||
sig_i = sig_p = None
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 일반 vol-lead
|
||||
close_2h = df.iloc[i - B_QUIET_CANDLES]["close"]
|
||||
chg_2h = abs(close - close_2h) / close_2h * 100
|
||||
quiet = chg_2h < B_QUIET_PCT
|
||||
spike = vol_r >= B_VOL_MULT
|
||||
|
||||
if quiet and spike:
|
||||
if sig_i is None: sig_i, sig_p = i, close
|
||||
else:
|
||||
if sig_i is not None and close < sig_p: sig_i = sig_p = None
|
||||
if sig_i is not None and (i - sig_i) > B_SIGNAL_TO:
|
||||
sig_i = sig_p = None
|
||||
if sig_i is not None and (close - sig_p) / sig_p * 100 >= B_THRESH:
|
||||
in_pos = True; buy_idx = i; buy_price = close
|
||||
stop_pct = calc_atr(df, i, B_ATR_CANDLES)
|
||||
entry_type = "일반"
|
||||
sig_i = sig_p = None
|
||||
i += 1
|
||||
return trades
|
||||
|
||||
|
||||
# ── 통계 계산 ─────────────────────────────────────────────
|
||||
def calc_stats(trades):
|
||||
if not trades:
|
||||
return {"n": 0, "wr": 0.0, "cum": 0.0, "dd": 0.0}
|
||||
wins = sum(1 for t in trades if t[0])
|
||||
cum = peak = dd = 0.0
|
||||
for t in sorted(trades, key=lambda x: x[2]):
|
||||
cum += t[1]
|
||||
peak = max(peak, cum)
|
||||
dd = max(dd, peak - cum)
|
||||
return {"n": len(trades), "wr": wins / len(trades) * 100, "cum": cum, "dd": dd}
|
||||
|
||||
|
||||
# ── 메인 ─────────────────────────────────────────────────
|
||||
def main():
|
||||
top30 = pickle.load(open(TOP30_FILE, "rb"))
|
||||
tickers = top30[:TOP_N]
|
||||
|
||||
print(f"{'='*60}")
|
||||
print(f"10분봉 전략 시뮬 | 최근 {SIM_DAYS}일 | {TOP_N}종목")
|
||||
print(f" A: 기존 1h봉 vol-lead")
|
||||
print(f" B: 10분봉 vol-lead + 극단거래량({B_EXTREME_VOL}x) 즉시 진입")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
print(f"데이터 로드 중...")
|
||||
data = load_data(tickers)
|
||||
|
||||
v1h = [t for t in tickers if t in data["1h"] and len(data["1h"][t]) >= 50]
|
||||
v10m = [t for t in tickers if t in data["10m"] and len(data["10m"][t]) >= 200]
|
||||
print(f"유효: 1h={len(v1h)}종목 / 10m={len(v10m)}종목\n")
|
||||
|
||||
# 전략 A
|
||||
a_all = []
|
||||
for t in v1h:
|
||||
a_all.extend(run_a(data["1h"][t]))
|
||||
|
||||
# 전략 B
|
||||
b_all = []
|
||||
for t in v10m:
|
||||
b_all.extend(run_b(data["10m"][t]))
|
||||
|
||||
b_extreme = [t for t in b_all if t[4] == "극단"]
|
||||
b_normal = [t for t in b_all if t[4] == "일반"]
|
||||
|
||||
sa = calc_stats(a_all)
|
||||
sb = calc_stats(b_all)
|
||||
sbe = calc_stats(b_extreme)
|
||||
sbn = calc_stats(b_normal)
|
||||
|
||||
print(f"{'='*65}")
|
||||
print(f"{'전략':18} {'거래수':>6} {'승률':>6} {'누적PnL%':>10} {'최대낙폭%':>10}")
|
||||
print(f"{'─'*65}")
|
||||
print(f"{'A (1h vol-lead)':18} {sa['n']:>6}건 {sa['wr']:>5.1f}% {sa['cum']:>+9.2f}% {-sa['dd']:>+9.2f}%")
|
||||
print(f"{'B 전체 (10m)':18} {sb['n']:>6}건 {sb['wr']:>5.1f}% {sb['cum']:>+9.2f}% {-sb['dd']:>+9.2f}%")
|
||||
print(f"{' └ 극단거래량':18} {sbe['n']:>6}건 {sbe['wr']:>5.1f}% {sbe['cum']:>+9.2f}% {-sbe['dd']:>+9.2f}%")
|
||||
print(f"{' └ 일반vol-lead':18} {sbn['n']:>6}건 {sbn['wr']:>5.1f}% {sbn['cum']:>+9.2f}% {-sbn['dd']:>+9.2f}%")
|
||||
print(f"{'='*65}")
|
||||
|
||||
# 극단 거래량 진입 상세
|
||||
if b_extreme:
|
||||
print(f"\n── 극단거래량 진입 상세 ({len(b_extreme)}건) ─────────────────────")
|
||||
print(f" {'매수시각':<18} {'PnL%':>7} {'청산'}")
|
||||
for t in sorted(b_extreme, key=lambda x: x[2])[:20]:
|
||||
mark = "✅" if t[0] else "❌"
|
||||
print(f" {str(t[2])[:16]:<18} {t[1]:>+6.1f}% {mark} {t[4]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user