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:
joungmin
2026-03-06 20:46:47 +09:00
parent 976c53ed66
commit 6e0c4508fa
69 changed files with 5018 additions and 495 deletions

View File

@@ -0,0 +1,131 @@
"""1년치 1분봉 OHLCV → BACKTEST_OHLCV (수동 페이지네이션).
pyupbit이 count>5000에서 실패하므로 to 파라미터로 직접 페이지네이션.
실행: .venv/bin/python3 tests/fetch_1y_minute1.py
예상 소요: 20종목 × ~15분 = ~5시간 (overnight 실행 권장)
"""
import sys, os, time
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 pyupbit
import oracledb
from datetime import datetime, timedelta
import pandas as pd
ALL_TICKERS = [
'KRW-XRP','KRW-BTC','KRW-ETH','KRW-SOL','KRW-DOGE', # 그룹 1
'KRW-ADA','KRW-SUI','KRW-NEAR','KRW-KAVA','KRW-SXP', # 그룹 2
'KRW-AKT','KRW-SONIC','KRW-IP','KRW-ORBS','KRW-VIRTUAL', # 그룹 3
'KRW-BARD','KRW-XPL','KRW-KITE','KRW-ENSO','KRW-0G', # 그룹 4
]
# 실행: python fetch_1y_minute1.py [그룹번호 1-4]
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('group', nargs='?', type=int, default=0,
help='1-4: 해당 그룹만 실행, 0: 전체')
args, _ = parser.parse_known_args()
if args.group in (1,2,3,4):
TICKERS = ALL_TICKERS[(args.group-1)*5 : args.group*5]
print(f"그룹 {args.group} 실행: {TICKERS}")
else:
TICKERS = ALL_TICKERS
BATCH = 4000 # 한 번에 요청할 봉 수 (4000 = ~2.8일)
DELAY = 0.15 # API 딜레이 (초)
TARGET_DAYS = 365 # 목표 기간
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 insert_batch(conn, ticker, df) -> int:
"""DB에 없는 행만 삽입. 반환: 신규 건수."""
cur = conn.cursor()
min_ts = df.index.min().to_pydatetime()
max_ts = df.index.max().to_pydatetime()
cur.execute(
"SELECT ts FROM backtest_ohlcv WHERE ticker=:t AND interval_cd='minute1' "
"AND ts BETWEEN :s AND :e",
{"t": ticker, "s": min_ts, "e": max_ts}
)
existing = {r[0] for r in cur.fetchall()}
new_rows = [
(ticker, 'minute1', ts.to_pydatetime(),
float(r["open"]), float(r["high"]), float(r["low"]),
float(r["close"]), float(r["volume"]))
for ts, r in df.iterrows()
if ts.to_pydatetime() not in existing
]
if not new_rows:
return 0
cur.executemany(
"INSERT INTO backtest_ohlcv (ticker,interval_cd,ts,open_p,high_p,low_p,close_p,volume_p) "
"VALUES (:1,:2,:3,:4,:5,:6,:7,:8)",
new_rows
)
conn.commit()
return len(new_rows)
def fetch_ticker(conn, ticker) -> int:
"""ticker 1년치 1분봉 fetch → DB 저장. 반환: 총 신규 건수."""
cutoff = datetime.now() - timedelta(days=TARGET_DAYS)
to_dt = datetime.now()
total = 0
batch_no = 0
while to_dt > cutoff:
to_str = to_dt.strftime('%Y-%m-%d %H:%M:%S')
try:
df = pyupbit.get_ohlcv(ticker, interval='minute1', count=BATCH, to=to_str)
time.sleep(DELAY)
except Exception as e:
print(f" API 오류: {e} → 재시도")
time.sleep(2.0)
continue
if df is None or len(df) == 0:
break
n = insert_batch(conn, ticker, df)
total += n
batch_no += 1
oldest = df.index[0]
print(f" 배치{batch_no:03d}: {oldest.date()} ~ {df.index[-1].strftime('%m-%d')} "
f"({len(df)}봉, 신규 {n}) | 누적 {total:,}", flush=True)
# 다음 페이지: 이 배치에서 가장 오래된 봉 이전부터
to_dt = oldest - timedelta(minutes=1)
if oldest <= cutoff:
break
return total
conn = _get_conn()
grand_total = 0
start_time = time.time()
for idx, tk in enumerate(TICKERS, 1):
t0 = time.time()
print(f"\n[{idx:02d}/{len(TICKERS)}] {tk} 시작...", flush=True)
try:
n = fetch_ticker(conn, tk)
elapsed = time.time() - t0
grand_total += n
print(f" → 완료: 신규 {n:,}행 ({elapsed/60:.1f}분) | 전체 누적 {grand_total:,}", flush=True)
except Exception as e:
print(f" → 오류: {e}")
conn.close()
elapsed_total = time.time() - start_time
print(f"\n전체 완료: {grand_total:,}행 저장 ({elapsed_total/60:.0f}분 소요)")