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:
355
archive/tests/sweep_volaccel.py
Normal file
355
archive/tests/sweep_volaccel.py
Normal file
@@ -0,0 +1,355 @@
|
||||
"""1분봉 볼륨 가속 전략 파라미터 스윕.
|
||||
|
||||
시그널 조건: N봉 연속으로 가격 AND 거래량이 함께 증가
|
||||
봉[n-(N-1)] < 봉[n-(N-2)] < ... < 봉[n]
|
||||
- 각 봉: 양봉 (close > open)
|
||||
- 가격 연속 상승: close[k] > close[k-1]
|
||||
- 볼륨 연속 증가: vol_ratio[k] > vol_ratio[k-1]
|
||||
- 현재봉(가장 강한 봉) vol_ratio >= VOL_MIN (pre-filter)
|
||||
|
||||
진입: 봉[n+1] close 즉시
|
||||
추적: 1분봉 trail stop (ATR) + time stop / 월별 배치
|
||||
"""
|
||||
import sys, os, itertools
|
||||
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 pandas as pd
|
||||
import oracledb
|
||||
import time as _time
|
||||
|
||||
# ── 고정 파라미터 ─────────────────────────────────────────────────────────────
|
||||
VOL_LOOKBACK = 61
|
||||
ATR_LOOKBACK = 28
|
||||
TS_N = 240
|
||||
TIME_STOP_PCT = 0.0 / 100
|
||||
FEE = 0.0005
|
||||
BUDGET = 15_000_000
|
||||
MAX_POS = 3
|
||||
PER_POS = BUDGET // MAX_POS
|
||||
|
||||
# ── 스윕 파라미터 ─────────────────────────────────────────────────────────────
|
||||
# 현재봉(가장 강한 봉)의 vol_ratio 최솟값 — 가속의 "끝점" 기준
|
||||
VOL_SWEEP = [2.0, 3.0, 4.0, 5.0, 8.0, 10.0, 15.0]
|
||||
ATR_SWEEP = {
|
||||
"ATR_MULT": [1.5, 2.0, 2.5, 3.0],
|
||||
"ATR_MIN": [0.005, 0.010, 0.015],
|
||||
"ATR_MAX": [0.020, 0.025, 0.030],
|
||||
}
|
||||
N_BARS_LIST = [2, 3, 4]
|
||||
VOL_MIN = min(VOL_SWEEP) # SQL pre-filter
|
||||
|
||||
# ── 시뮬 구간 ─────────────────────────────────────────────────────────────────
|
||||
SIM_START = datetime(2025, 8, 1)
|
||||
SIM_END = datetime(2026, 3, 4)
|
||||
WARMUP_MINS = 120
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _months(start: datetime, end: datetime):
|
||||
m = start.replace(day=1)
|
||||
while m < end:
|
||||
nxt = (m + timedelta(days=32)).replace(day=1)
|
||||
if nxt > end:
|
||||
nxt = end
|
||||
yield m, nxt
|
||||
m = nxt
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# ── N별 SQL 생성 ──────────────────────────────────────────────────────────────
|
||||
# 조건: 가격 연속 상승 + 볼륨 연속 증가 (+ 양봉)
|
||||
# vol_ratio_0 > vol_ratio_1 > ... > vol_ratio_(n-1)
|
||||
# close_0 > close_1 > ... > close_(n-1)
|
||||
# 모두 Oracle SQL에서 처리
|
||||
|
||||
def build_sql(n: int) -> str:
|
||||
lag_cols = "\n".join(
|
||||
f" LAG(close_p, {i}) OVER (PARTITION BY ticker ORDER BY ts) prev_close_{i},\n"
|
||||
f" LAG(open_p, {i}) OVER (PARTITION BY ticker ORDER BY ts) prev_open_{i},\n"
|
||||
f" LAG(volume_p, {i}) OVER (PARTITION BY ticker ORDER BY ts) prev_vol_{i},"
|
||||
for i in range(1, n)
|
||||
)
|
||||
|
||||
vr_cols = []
|
||||
vr_cols.append(f""" volume_p / NULLIF(
|
||||
AVG(volume_p) OVER (PARTITION BY ticker ORDER BY ts
|
||||
ROWS BETWEEN {VOL_LOOKBACK} PRECEDING AND 2 PRECEDING), 0
|
||||
) vol_ratio_0,""")
|
||||
for i in range(1, n):
|
||||
vr_cols.append(f""" prev_vol_{i} / NULLIF(
|
||||
AVG(volume_p) OVER (PARTITION BY ticker ORDER BY ts
|
||||
ROWS BETWEEN {VOL_LOOKBACK} PRECEDING AND 2 PRECEDING), 0
|
||||
) vol_ratio_{i},""")
|
||||
vr_cols_str = "\n".join(vr_cols)
|
||||
|
||||
lag_passthrough = "\n".join(
|
||||
f" prev_close_{i}, prev_open_{i},"
|
||||
for i in range(1, n)
|
||||
)
|
||||
|
||||
# 조건: 양봉 + 가격 연속 상승 + 볼륨 연속 증가
|
||||
cond_lines = [
|
||||
" AND vol_ratio_0 >= :min_vol", # pre-filter: 현재봉(최강봉)
|
||||
" AND close_p > open_p", # 현재봉 양봉
|
||||
]
|
||||
for i in range(1, n):
|
||||
cond_lines.append(f" AND prev_close_{i} > prev_open_{i}") # 이전봉 양봉
|
||||
if i == 1:
|
||||
cond_lines.append(f" AND close_p > prev_close_{i}") # 가격 상승
|
||||
cond_lines.append(f" AND vol_ratio_0 > vol_ratio_{i}") # 볼륨 증가
|
||||
else:
|
||||
cond_lines.append(f" AND prev_close_{i-1} > prev_close_{i}") # 가격 상승
|
||||
cond_lines.append(f" AND vol_ratio_{i-1} > vol_ratio_{i}") # 볼륨 증가
|
||||
cond_str = "\n".join(cond_lines)
|
||||
|
||||
vr_select = ", ".join(f"vol_ratio_{i}" for i in range(n))
|
||||
|
||||
return f"""
|
||||
WITH
|
||||
base AS (
|
||||
SELECT ticker, ts, open_p, close_p, high_p, low_p, volume_p,
|
||||
{lag_cols}
|
||||
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(:load_since, 'YYYY-MM-DD HH24:MI:SS')
|
||||
AND ts < TO_TIMESTAMP(:sim_end, 'YYYY-MM-DD HH24:MI:SS')
|
||||
AND ticker IN ({_TK})
|
||||
),
|
||||
indicators AS (
|
||||
SELECT ticker, ts, open_p, close_p,
|
||||
{lag_passthrough}
|
||||
{vr_cols_str}
|
||||
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
|
||||
),
|
||||
signals AS (
|
||||
SELECT ticker, ts sig_ts, close_p sig_price,
|
||||
{vr_select}, atr_raw
|
||||
FROM indicators
|
||||
WHERE ts >= TO_TIMESTAMP(:sim_start, 'YYYY-MM-DD HH24:MI:SS')
|
||||
AND ts < TO_TIMESTAMP(:sim_end, 'YYYY-MM-DD HH24:MI:SS')
|
||||
{cond_str}
|
||||
),
|
||||
entry_cands AS (
|
||||
SELECT s.ticker, s.sig_ts,
|
||||
{vr_select.replace('vol_ratio_', 's.vol_ratio_')}, s.atr_raw,
|
||||
e.ts entry_ts, e.close_p entry_price,
|
||||
ROW_NUMBER() OVER (PARTITION BY s.ticker, s.sig_ts ORDER BY e.ts) rn
|
||||
FROM signals s
|
||||
JOIN backtest_ohlcv e
|
||||
ON e.ticker = s.ticker
|
||||
AND e.interval_cd = 'minute1'
|
||||
AND e.ts > s.sig_ts
|
||||
AND e.ts <= s.sig_ts + INTERVAL '3' MINUTE
|
||||
),
|
||||
entries AS (
|
||||
SELECT ticker, sig_ts, {vr_select}, atr_raw, entry_ts, entry_price
|
||||
FROM entry_cands WHERE rn = 1
|
||||
),
|
||||
post_entry AS (
|
||||
SELECT
|
||||
e.ticker, e.sig_ts, e.entry_ts, e.entry_price,
|
||||
{vr_select.replace('vol_ratio_', 'e.vol_ratio_')}, e.atr_raw,
|
||||
b.close_p bar_price,
|
||||
ROW_NUMBER() OVER (PARTITION BY e.ticker, e.entry_ts ORDER BY b.ts) bar_n,
|
||||
MAX(b.close_p) OVER (PARTITION BY e.ticker, e.entry_ts
|
||||
ORDER BY b.ts ROWS UNBOUNDED PRECEDING) running_peak
|
||||
FROM entries e
|
||||
JOIN backtest_ohlcv b
|
||||
ON b.ticker = e.ticker
|
||||
AND b.interval_cd = 'minute1'
|
||||
AND b.ts >= e.entry_ts
|
||||
AND b.ts <= e.entry_ts + INTERVAL '{TS_N}' MINUTE
|
||||
)
|
||||
SELECT ticker, sig_ts, entry_ts, entry_price,
|
||||
{vr_select}, atr_raw,
|
||||
bar_n, bar_price, running_peak
|
||||
FROM post_entry
|
||||
WHERE bar_n <= :ts_n + 1
|
||||
ORDER BY ticker, entry_ts, bar_n
|
||||
"""
|
||||
|
||||
|
||||
# ── 월별 데이터 로드 ──────────────────────────────────────────────────────────
|
||||
print(f"볼륨 가속 전략 (현재봉 VOL>={VOL_MIN}x, N={N_BARS_LIST}, 가격·볼륨 동시 가속)\n", flush=True)
|
||||
|
||||
conn = _get_conn()
|
||||
cur = conn.cursor()
|
||||
cur.arraysize = 100_000
|
||||
|
||||
ALL_ENTRIES: dict[int, dict] = {n: {} for n in N_BARS_LIST}
|
||||
t_load = _time.time()
|
||||
|
||||
for n in N_BARS_LIST:
|
||||
sql = build_sql(n)
|
||||
print(f"── {n}봉 로드 중... ──────────────────────────", flush=True)
|
||||
n_total = 0
|
||||
|
||||
for m_start, m_end in _months(SIM_START, SIM_END):
|
||||
load_since = (m_start - timedelta(minutes=WARMUP_MINS)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
sim_start = m_start.strftime('%Y-%m-%d %H:%M:%S')
|
||||
sim_end = m_end.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
t0 = _time.time()
|
||||
cur.execute(sql, {
|
||||
"load_since": load_since,
|
||||
"sim_start": sim_start,
|
||||
"sim_end": sim_end,
|
||||
"min_vol": VOL_MIN,
|
||||
"ts_n": TS_N,
|
||||
})
|
||||
rows = cur.fetchall()
|
||||
t1 = _time.time()
|
||||
|
||||
n_new = 0
|
||||
for row in rows:
|
||||
ticker = row[0]
|
||||
entry_ts = row[2]
|
||||
entry_price = float(row[3])
|
||||
vr_vals = [float(row[4 + i]) if row[4 + i] is not None else 0.0 for i in range(n)]
|
||||
atr_raw = row[4 + n]
|
||||
bar_price = float(row[4 + n + 2])
|
||||
running_peak= float(row[4 + n + 3])
|
||||
|
||||
key = (ticker, entry_ts)
|
||||
if key not in ALL_ENTRIES[n]:
|
||||
ALL_ENTRIES[n][key] = {
|
||||
'entry_price': entry_price,
|
||||
'vr0': vr_vals[0], # 현재봉 vol (가장 강한 봉)
|
||||
'atr_raw': float(atr_raw) if atr_raw is not None else float('nan'),
|
||||
'bars': [],
|
||||
}
|
||||
n_new += 1
|
||||
ALL_ENTRIES[n][key]['bars'].append((bar_price, running_peak))
|
||||
|
||||
n_total += n_new
|
||||
print(f" {sim_start[:7]}: {len(rows):>8,}행 ({t1-t0:.1f}s) | 진입 {n_new:>5}건", flush=True)
|
||||
|
||||
print(f" → {n}봉 합계: {n_total}건\n", flush=True)
|
||||
|
||||
conn.close()
|
||||
print(f"전체 로드 완료 ({_time.time()-t_load:.1f}s)\n", flush=True)
|
||||
|
||||
|
||||
# ── 출구 탐색 ─────────────────────────────────────────────────────────────────
|
||||
def find_exit(entry_price: float, atr_stop: float, bars: list) -> float:
|
||||
for i, (bp, pk) in enumerate(bars):
|
||||
drop = (pk - bp) / pk if pk > 0 else 0.0
|
||||
pnl = (bp - entry_price) / entry_price
|
||||
if drop >= atr_stop:
|
||||
return pnl * 100
|
||||
if i + 1 >= TS_N and pnl < TIME_STOP_PCT:
|
||||
return pnl * 100
|
||||
return (bars[-1][0] - entry_price) / entry_price * 100 if bars else 0.0
|
||||
|
||||
|
||||
# ── 스윕 ──────────────────────────────────────────────────────────────────────
|
||||
atr_keys = list(ATR_SWEEP.keys())
|
||||
atr_combos = list(itertools.product(*ATR_SWEEP.values()))
|
||||
total_combos = len(N_BARS_LIST) * len(VOL_SWEEP) * len(atr_combos)
|
||||
print(f"총 {total_combos}가지 조합 스윕...\n", flush=True)
|
||||
|
||||
t_sweep = _time.time()
|
||||
results = []
|
||||
|
||||
for n in N_BARS_LIST:
|
||||
entry_list = list(ALL_ENTRIES[n].values())
|
||||
for vol in VOL_SWEEP:
|
||||
for atr_combo in atr_combos:
|
||||
atr_params = dict(zip(atr_keys, atr_combo))
|
||||
if atr_params['ATR_MIN'] >= atr_params['ATR_MAX']:
|
||||
continue
|
||||
|
||||
atr_mult = atr_params['ATR_MULT']
|
||||
atr_min = atr_params['ATR_MIN']
|
||||
atr_max = atr_params['ATR_MAX']
|
||||
|
||||
trades = []
|
||||
for e in entry_list:
|
||||
# 현재봉(최강봉) vol 기준으로 필터
|
||||
if e['vr0'] < vol:
|
||||
continue
|
||||
|
||||
ar = e['atr_raw']
|
||||
atr_s = (atr_min if (ar != ar)
|
||||
else max(atr_min, min(atr_max, ar * atr_mult)))
|
||||
pnl_pct = find_exit(e['entry_price'], atr_s, e['bars'])
|
||||
krw = PER_POS * (pnl_pct / 100) - PER_POS * FEE * 2
|
||||
trades.append((pnl_pct, krw))
|
||||
|
||||
if not trades:
|
||||
results.append({'N_BARS': n, 'VOL': vol, **atr_params,
|
||||
'trades': 0, 'wins': 0,
|
||||
'win_rate': 0.0, 'avg_pnl': 0.0, 'total_krw': 0.0})
|
||||
continue
|
||||
|
||||
wins = sum(1 for p, _ in trades if p > 0)
|
||||
results.append({
|
||||
'N_BARS': n,
|
||||
'VOL': vol,
|
||||
**atr_params,
|
||||
'trades': len(trades),
|
||||
'wins': wins,
|
||||
'win_rate': wins / len(trades) * 100,
|
||||
'avg_pnl': sum(p for p, _ in trades) / len(trades),
|
||||
'total_krw': sum(k for _, k in trades),
|
||||
})
|
||||
|
||||
print(f"스윕 완료 ({_time.time()-t_sweep:.1f}s)\n")
|
||||
|
||||
# ── 결과 출력 ─────────────────────────────────────────────────────────────────
|
||||
df_r = pd.DataFrame(results)
|
||||
df_r = df_r[df_r['trades'] > 0].sort_values('total_krw', ascending=False)
|
||||
|
||||
print("=" * 105)
|
||||
print(f"{'순위':>4} {'N봉':>4} {'VOL':>5} {'ATR_M':>6} {'ATR_N':>6} {'ATR_X':>6} "
|
||||
f"{'건수':>5} {'승률':>6} {'평균PNL':>8} {'총손익':>14}")
|
||||
print("=" * 105)
|
||||
for rank, (_, row) in enumerate(df_r.head(30).iterrows(), 1):
|
||||
print(f"{rank:>4} {int(row['N_BARS']):>2}봉 {row['VOL']:>4.1f}x {row['ATR_MULT']:>6.1f} "
|
||||
f"{row['ATR_MIN']*100:>5.1f}% {row['ATR_MAX']*100:>5.1f}% "
|
||||
f"{int(row['trades']):>5}건 {row['win_rate']:>5.0f}% "
|
||||
f"{row['avg_pnl']:>+7.2f}% {row['total_krw']:>+14,.0f}원")
|
||||
|
||||
# N봉 × VOL별 최상위 요약
|
||||
print("\n" + "─" * 85)
|
||||
print(f" {'N봉':>3} {'VOL':>5} {'건수':>5} {'승률':>5} {'평균PNL':>8} {'총손익':>14} (최적 ATR)")
|
||||
print("─" * 85)
|
||||
for n in N_BARS_LIST:
|
||||
for vol in VOL_SWEEP:
|
||||
sub = df_r[(df_r['N_BARS'] == n) & (df_r['VOL'] == vol)]
|
||||
if sub.empty:
|
||||
continue
|
||||
best = sub.iloc[0]
|
||||
if best['trades'] == 0:
|
||||
continue
|
||||
print(f" {int(n):>2}봉 {vol:>4.1f}x {int(best['trades']):>5}건 {best['win_rate']:>4.0f}% "
|
||||
f"{best['avg_pnl']:>+7.2f}% {best['total_krw']:>+14,.0f}원 "
|
||||
f"(M={best['ATR_MULT']:.1f} N={best['ATR_MIN']*100:.1f}% X={best['ATR_MAX']*100:.1f}%)")
|
||||
print()
|
||||
Reference in New Issue
Block a user