feat: add backtest module with DB cache and scenario comparison

Backtest improvements:
- Add backtest.py with Oracle DB-backed OHLCV cache (no repeated API calls)
- Add backtest_trades table to cache simulation results by params hash
  (same params -> instant load, skip re-simulation)
- Add walk-forward scenario comparison (--walkforward-cmp)
- Add trend ceiling filter (--trend-cmp, max gain threshold)
- Add ticker win-rate filter (--ticker-cmp, SQL-based instant analysis)
- Precompute daily_features once per data load (not per scenario)

Live bot fixes:
- monitor: add hard stop-loss from buy price (in addition to trailing)
- strategy: fix re-entry condition to require +1% above last sell price
- price_collector: add 48h backfill on startup for trend calculation
- main: call backfill_prices() at startup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-02-28 23:28:27 +09:00
parent 4888aa0faa
commit 0b264b304c
5 changed files with 1621 additions and 17 deletions

View File

@@ -5,10 +5,11 @@ from __future__ import annotations
import logging
import time
import pyupbit
import requests
from .market import get_top_tickers
from .price_db import cleanup_old_prices, insert_prices
from .price_db import cleanup_old_prices, insert_prices, insert_prices_with_time
logger = logging.getLogger(__name__)
@@ -16,6 +17,38 @@ COLLECT_INTERVAL = 600 # 10분 (초)
CLEANUP_EVERY = 6 # 1시간(10분 × 6)마다 오래된 데이터 정리
def backfill_prices(hours: int = 48) -> None:
"""시작 시 과거 N시간치 1시간봉 종가를 DB에 백필.
price_history에 데이터가 없으면 추세 판단이 불가능하므로
봇 시작 직후 한 번 호출해 과거 데이터를 채운다.
"""
tickers = get_top_tickers()
if not tickers:
logger.warning("[백필] 종목 목록 없음, 스킵")
return
count = hours + 2 # 여유 있게 요청
total_rows = 0
for ticker in tickers:
try:
df = pyupbit.get_ohlcv(ticker, interval="minute60", count=count)
if df is None or df.empty:
continue
rows = [
(ticker, float(row["close"]), ts.to_pydatetime())
for ts, row in df.iterrows()
]
insert_prices_with_time(rows)
total_rows += len(rows)
time.sleep(0.1)
except Exception as e:
logger.error(f"[백필] {ticker} 오류: {e}")
logger.info(f"[백필] 완료 — {len(tickers)}개 종목 / {total_rows}개 레코드 저장")
def run_collector(interval: int = COLLECT_INTERVAL) -> None:
"""가격 수집 루프."""
logger.info(f"가격 수집기 시작 (주기={interval//60}분)")