Files
upbit-trader/archive/tests/check_recent_signals.py
joungmin 6e0c4508fa 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>
2026-03-06 20:46:47 +09:00

361 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""최근 N일 볼륨 가속 시그널 확인 + ATR trail stop 시뮬레이션.
4봉 연속 가격+볼륨 가속 시그널 발생 후 실제 trail stop 로직을 돌려
진입가·청산가·PNL을 표시.
최적 파라미터 (sweep_volaccel 기준):
N봉=4, VOL≥5.0x, ATR_MULT=1.5, ATR_MIN=1.5%, ATR_MAX=2.0%
"""
import sys, os
from datetime import datetime, timedelta
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.env'))
import oracledb
# ── 파라미터 ──────────────────────────────────────────────────────────────────
VOL_LOOKBACK = 61
ATR_LOOKBACK = 28
LOOKBACK_DAYS = 3 # 오늘 기준 며칠 전까지 시그널 탐색
VOL_MIN_LIST = [8.0]
VOL_MIN = min(VOL_MIN_LIST)
# ATR 비교 목록: (MULT, MIN%, MAX%) — 타이트→여유 순
ATR_SCENARIOS = [
(1.0, 0.010, 0.020), # 타이트: 1.0×ATR, 1.0~2.0%
(1.5, 0.015, 0.025), # 기존최적: 1.5×ATR, 1.5~2.5%
(1.0, 0.020, 0.030), # 고정 2.0~3.0%
(1.0, 0.030, 0.050), # 느슨: 3.0~5.0%
]
ATR_MULT = ATR_SCENARIOS[0][0]
ATR_MIN_R = ATR_SCENARIOS[0][1]
ATR_MAX_R = ATR_SCENARIOS[0][2]
MAX_TRAIL_BARS = 240
BUDGET = 15_000_000
MAX_POS = 3
PER_POS = BUDGET // MAX_POS
FEE = 0.0005
TICKERS = [
'KRW-XRP','KRW-BTC','KRW-ETH','KRW-SOL','KRW-DOGE',
'KRW-ADA','KRW-SUI','KRW-NEAR','KRW-KAVA','KRW-SXP',
'KRW-AKT','KRW-SONIC','KRW-IP','KRW-ORBS','KRW-VIRTUAL',
'KRW-BARD','KRW-XPL','KRW-KITE','KRW-ENSO','KRW-0G',
]
_TK = ",".join(f"'{t}'" for t in TICKERS)
# ── 시그널 SQL (4봉 가속 조건 전부 Oracle SQL에서 처리) ───────────────────────
SIGNAL_SQL = f"""
WITH
base AS (
SELECT ticker, ts, open_p, close_p, high_p, low_p, volume_p,
LAG(close_p, 1) OVER (PARTITION BY ticker ORDER BY ts) prev_close_1,
LAG(open_p, 1) OVER (PARTITION BY ticker ORDER BY ts) prev_open_1,
LAG(volume_p, 1) OVER (PARTITION BY ticker ORDER BY ts) prev_vol_1,
LAG(close_p, 2) OVER (PARTITION BY ticker ORDER BY ts) prev_close_2,
LAG(open_p, 2) OVER (PARTITION BY ticker ORDER BY ts) prev_open_2,
LAG(volume_p, 2) OVER (PARTITION BY ticker ORDER BY ts) prev_vol_2,
LAG(close_p, 3) OVER (PARTITION BY ticker ORDER BY ts) prev_close_3,
LAG(open_p, 3) OVER (PARTITION BY ticker ORDER BY ts) prev_open_3,
LAG(volume_p, 3) OVER (PARTITION BY ticker ORDER BY ts) prev_vol_3,
GREATEST(
high_p - low_p,
ABS(high_p - LAG(close_p,1) OVER (PARTITION BY ticker ORDER BY ts)),
ABS(low_p - LAG(close_p,1) OVER (PARTITION BY ticker ORDER BY ts))
) tr
FROM backtest_ohlcv
WHERE interval_cd = 'minute1'
AND ts >= TO_TIMESTAMP(:warmup_since, 'YYYY-MM-DD HH24:MI:SS')
AND ticker IN ({_TK})
),
indicators AS (
SELECT ticker, ts, open_p, close_p,
volume_p / NULLIF(
AVG(volume_p) OVER (PARTITION BY ticker ORDER BY ts
ROWS BETWEEN {VOL_LOOKBACK} PRECEDING AND 2 PRECEDING), 0
) vr0,
prev_vol_1 / NULLIF(
AVG(volume_p) OVER (PARTITION BY ticker ORDER BY ts
ROWS BETWEEN {VOL_LOOKBACK} PRECEDING AND 2 PRECEDING), 0
) vr1,
prev_vol_2 / NULLIF(
AVG(volume_p) OVER (PARTITION BY ticker ORDER BY ts
ROWS BETWEEN {VOL_LOOKBACK} PRECEDING AND 2 PRECEDING), 0
) vr2,
prev_vol_3 / NULLIF(
AVG(volume_p) OVER (PARTITION BY ticker ORDER BY ts
ROWS BETWEEN {VOL_LOOKBACK} PRECEDING AND 2 PRECEDING), 0
) vr3,
prev_close_1, prev_open_1,
prev_close_2, prev_open_2,
prev_close_3, prev_open_3,
AVG(tr) OVER (PARTITION BY ticker ORDER BY ts
ROWS BETWEEN {ATR_LOOKBACK} PRECEDING AND 1 PRECEDING)
/ NULLIF(prev_close_1, 0) atr_raw
FROM base
)
SELECT ticker, ts sig_ts, close_p sig_price,
vr0, vr1, vr2, vr3, atr_raw,
prev_close_3, prev_close_2, prev_close_1, close_p
FROM indicators
WHERE ts >= TO_TIMESTAMP(:check_since, 'YYYY-MM-DD HH24:MI:SS')
AND vr0 >= :min_vol
-- 4봉 연속: 양봉
AND close_p > open_p
AND prev_close_1 > prev_open_1
AND prev_close_2 > prev_open_2
AND prev_close_3 > prev_open_3
-- 4봉 연속: 가격 가속
AND close_p > prev_close_1
AND prev_close_1 > prev_close_2
AND prev_close_2 > prev_close_3
-- 4봉 연속: 볼륨 가속
AND vr0 > vr1
AND vr1 > vr2
AND vr2 > vr3
ORDER BY ticker, ts
"""
def _get_conn():
kwargs = dict(user=os.environ["ORACLE_USER"],
password=os.environ["ORACLE_PASSWORD"],
dsn=os.environ["ORACLE_DSN"])
wallet = os.environ.get("ORACLE_WALLET")
if wallet:
kwargs["config_dir"] = wallet
return oracledb.connect(**kwargs)
def simulate_trail(cur, ticker: str, entry_ts, entry_price: float,
atr_raw: float,
mult: float = None, min_r: float = None, max_r: float = None) -> dict:
"""entry_ts 봉부터 trail stop 시뮬. 상세 결과 dict 반환."""
m = mult if mult is not None else ATR_MULT
n = min_r if min_r is not None else ATR_MIN_R
x = max_r if max_r is not None else ATR_MAX_R
ar = atr_raw if (atr_raw and atr_raw == atr_raw) else 0.0
atr_stop = max(n, min(x, ar * m)) if ar > 0 else x
cur.execute(
"""SELECT ts, close_p FROM backtest_ohlcv
WHERE ticker=:t AND interval_cd='minute1'
AND ts >= :entry
ORDER BY ts FETCH FIRST :n ROWS ONLY""",
{"t": ticker, "entry": entry_ts, "n": MAX_TRAIL_BARS + 1}
)
bars = cur.fetchall()
if not bars:
return dict(status="데이터없음", exit_ts=entry_ts, exit_price=entry_price,
peak=entry_price, peak_pct=0.0, pnl=0.0, atr_stop=atr_stop,
held_bars=0)
running_peak = entry_price
for i, (ts, close_p) in enumerate(bars):
close_p = float(close_p)
running_peak = max(running_peak, close_p)
drop = (running_peak - close_p) / running_peak if running_peak > 0 else 0.0
if drop >= atr_stop:
pnl = (close_p - entry_price) / entry_price * 100
peak_pct = (running_peak - entry_price) / entry_price * 100
return dict(status="손절", exit_ts=ts, exit_price=close_p,
peak=running_peak, peak_pct=peak_pct,
pnl=pnl, atr_stop=atr_stop, held_bars=i + 1)
last_ts, last_price = bars[-1][0], float(bars[-1][1])
pnl = (last_price - entry_price) / entry_price * 100
peak_pct = (running_peak - entry_price) / entry_price * 100
status = "타임아웃" if len(bars) >= MAX_TRAIL_BARS else "진행중"
return dict(status=status, exit_ts=last_ts, exit_price=last_price,
peak=running_peak, peak_pct=peak_pct,
pnl=pnl, atr_stop=atr_stop, held_bars=len(bars))
def apply_pos_limit(sim_results: list, vol_thr: float) -> tuple:
"""VOL 필터 + MAX_POS 동시 포지션 제한 적용. (taken, skipped) 반환."""
filtered = [r for r in sim_results if r['vr'][3] >= vol_thr]
open_positions = []
taken, skipped = [], []
for r in filtered:
open_positions = [ex for ex in open_positions if ex > r['entry_ts']]
if len(open_positions) < MAX_POS:
open_positions.append(r['exit_ts'])
taken.append(r)
else:
skipped.append(r)
return taken, skipped
def print_detail(vol_thr: float, taken: list, skipped: list):
div = "" * 120
print(f"\n{''*120}")
print(f" 4봉 VOL≥{vol_thr:.0f}x | ATR={ATR_MULT}×[{ATR_MIN_R*100:.1f}~{ATR_MAX_R*100:.1f}%] "
f"| 자본 {BUDGET//10000}만원 / 포지션 {PER_POS//10000}만원 / 동시 {MAX_POS}")
print(f"{''*120}\n")
total_krw, total_wins = 0.0, 0
for i, r in enumerate(taken, 1):
vr = r['vr']
entry_str = str(r['entry_ts'])[:16]
exit_str = str(r['exit_ts'])[:16]
krw = PER_POS * (r['pnl'] / 100) - PER_POS * FEE * 2
total_krw += krw
won = r['pnl'] > 0
if won:
total_wins += 1
pr = r['prices']
# 봉별 가격 변화율 (봉1→2, 2→3, 3→4)
pchg = [
(pr[1] - pr[0]) / pr[0] * 100 if pr[0] else 0,
(pr[2] - pr[1]) / pr[1] * 100 if pr[1] else 0,
(pr[3] - pr[2]) / pr[2] * 100 if pr[2] else 0,
]
sign = "" if won else ""
print(f" #{i:02d} {r['ticker']:12s} [{sign}]")
print(f" 가격흐름 {pr[0]:,.0f}{pr[1]:,.0f} ({pchg[0]:+.2f}%)"
f"{pr[2]:,.0f} ({pchg[1]:+.2f}%)"
f"{pr[3]:,.0f} ({pchg[2]:+.2f}%)")
print(f" 볼륨흐름 {vr[0]:.1f}x → {vr[1]:.1f}x → {vr[2]:.1f}x → {vr[3]:.1f}x "
f"(ATR손절 {r['atr_stop']*100:.1f}%)")
print(f" 진입 {entry_str} @ {r['entry_price']:>13,.0f}")
print(f" 고점 {r['peak']:>13,.0f}원 ({r['peak_pct']:>+.2f}%까지 상승)")
print(f" 청산 {exit_str} @ {r['exit_price']:>13,.0f}"
f" ({r['status']}, {r['held_bars']}봉 보유)")
print(f" 손익 {r['pnl']:>+.2f}% → {krw:>+,.0f}")
print()
if skipped:
print(f" ── 스킵 {len(skipped)}건 (포지션 한도 초과) ──")
for r in skipped:
vr = r['vr']
krw_if = PER_POS * (r['pnl'] / 100) - PER_POS * FEE * 2
print(f" SKIP {r['ticker']:12s} 진입 {str(r['entry_ts'])[:16]} "
f"vol {vr[3]:.1f}x → {r['pnl']:>+.2f}% ({krw_if:>+,.0f}원 기회손실)")
print()
n = len(taken)
win_rate = total_wins / n * 100 if n else 0
ret_rate = total_krw / BUDGET * 100
print(div)
print(f" ◆ 실거래 {n}건 | 승률 {win_rate:.0f}% ({total_wins}/{n}) | "
f"합산 {total_krw:>+,.0f}원 | 수익률 {ret_rate:>+.2f}%")
print(div)
def main():
now = datetime.now()
check_since = (now - timedelta(days=LOOKBACK_DAYS)).strftime('%Y-%m-%d 00:00:00')
warmup_since = (now - timedelta(days=LOOKBACK_DAYS + 1)).strftime('%Y-%m-%d 00:00:00')
print(f"=== 4봉 볼륨 가속 시뮬 — VOL {VOL_MIN_LIST} 비교 ===")
print(f"ATR={ATR_MULT}×[{ATR_MIN_R*100:.1f}~{ATR_MAX_R*100:.1f}%], "
f"자본 {BUDGET/10000:.0f}만원 / 포지션당 {PER_POS/10000:.0f}만원 / 동시 {MAX_POS}")
print(f"조회 기간: {check_since[:10]} ~ 현재\n")
conn = _get_conn()
cur = conn.cursor()
cur.arraysize = 10_000
# ── 1회 조회 (VOL_MIN = 가장 낮은 값) ────────────────────────────────────────
cur.execute(SIGNAL_SQL, {
"warmup_since": warmup_since,
"check_since": check_since,
"min_vol": VOL_MIN,
})
rows = cur.fetchall()
if not rows:
print(f"해당 기간 VOL>={VOL_MIN}x 4봉 가속 시그널 없음")
conn.close()
return
# 진입봉 조회
signals = []
for row in rows:
ticker, sig_ts, sig_price, vr0, vr1, vr2, vr3, atr_raw, \
p3, p2, p1, p0 = row
cur.execute(
"""SELECT close_p, ts FROM backtest_ohlcv
WHERE ticker=:t AND interval_cd='minute1'
AND ts > :sig AND ts <= :sig + INTERVAL '3' MINUTE
ORDER BY ts FETCH FIRST 1 ROWS ONLY""",
{"t": ticker, "sig": sig_ts}
)
entry_row = cur.fetchone()
if not entry_row:
continue
entry_price, entry_ts = float(entry_row[0]), entry_row[1]
signals.append({
'ticker': ticker,
'sig_ts': sig_ts,
'entry_ts': entry_ts,
'entry_price': entry_price,
'vr': [float(x) if x else 0.0 for x in [vr3, vr2, vr1, vr0]],
'prices': [float(x) if x else 0.0 for x in [p3, p2, p1, p0]],
'atr_raw': float(atr_raw) if atr_raw else 0.0,
})
if not signals:
print("진입봉을 찾을 수 없음")
conn.close()
return
# ── trail stop 시뮬 — 시나리오별 ─────────────────────────────────────────────
signals.sort(key=lambda x: x['entry_ts'])
vol_thr = VOL_MIN_LIST[0]
# 각 시나리오에 맞는 bars를 재사용하기 위해 기본(가장 느슨한) 시나리오로 bars 캐시
# (atr_stop 달라도 bars 조회는 동일하므로 1회만 fetch)
print(f"시그널 {len(signals)}건 trail stop 시뮬 중 ({len(ATR_SCENARIOS)}가지 ATR 비교)...", flush=True)
# 시나리오별 결과 수집
scenario_results = []
for mult, min_r, max_r in ATR_SCENARIOS:
sim = []
for s in signals:
trail = simulate_trail(cur, s['ticker'], s['entry_ts'], s['entry_price'],
s['atr_raw'], mult=mult, min_r=min_r, max_r=max_r)
sim.append({**s, **trail})
taken, skipped = apply_pos_limit(sim, vol_thr)
scenario_results.append((mult, min_r, max_r, taken, skipped))
# ── 시나리오 요약 비교표 ──────────────────────────────────────────────────────
print(f"\n{''*95}")
print(f" {'ATR 설정':22s} {'거래':>4s} {'승률':>5s} {'합산손익':>12s} {'수익률':>6s} {'평균보유(분)':>10s}")
print(f"{''*95}")
best_krw = max(sum(PER_POS*(r['pnl']/100) - PER_POS*FEE*2 for r in taken)
for _, _, _, taken, _ in scenario_results)
for mult, min_r, max_r, taken, skipped in scenario_results:
n = len(taken)
if n == 0:
continue
wins = sum(1 for r in taken if r['pnl'] > 0)
total = sum(PER_POS*(r['pnl']/100) - PER_POS*FEE*2 for r in taken)
avg_held = sum(r['held_bars'] for r in taken) / n
ret = total / BUDGET * 100
label = f"{mult:.1f}×ATR [{min_r*100:.1f}~{max_r*100:.1f}%]"
star = "" if abs(total - best_krw) < 1 else ""
print(f" {label:22s} {n:>4d}{wins/n*100:>4.0f}% {total:>+12,.0f}{ret:>+5.2f}%"
f" {avg_held:>6.0f}{star}")
print(f"{''*95}")
# ── 최적 시나리오 건별 상세 출력 ─────────────────────────────────────────────
best_idx = max(range(len(scenario_results)),
key=lambda i: sum(PER_POS*(r['pnl']/100) - PER_POS*FEE*2
for r in scenario_results[i][3]))
bm, bn, bx, best_taken, best_skipped = scenario_results[best_idx]
print(f"\n[최적 시나리오 건별 상세: {bm:.1f}×ATR {bn*100:.1f}~{bx*100:.1f}%]")
print_detail(vol_thr, best_taken, best_skipped)
conn.close()
if __name__ == '__main__':
main()