refactor: DeepCoin 1·2단계 파이프라인으로 구조 재편
레거시 분석·매칭·운영 코드를 정리하고 src/deepcoin 기반으로 재구성한다. 1단계 GT는 2년 스윙·눌림목·돌파·다이버전스 타점을 차트에 표시하고, 2단계는 8개 매매 기법과 GT 정합 평가 스크립트를 추가한다. .env와 GT JSON 산출물은 추적에서 제외한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
161
src/deepcoin/data/downloader.py
Normal file
161
src/deepcoin/data/downloader.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""빗썸 캔들 역방향 페이지네이션 다운로더."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.api.bithumb import BithumbCandleClient, parse_kst_datetime
|
||||
from deepcoin.config import Settings
|
||||
from deepcoin.data.candle_store import CandleStore, candle_to_row
|
||||
from deepcoin.data.intervals import interval_label
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DownloadResult:
|
||||
"""단일 인터벌 다운로드 결과."""
|
||||
|
||||
interval_min: int
|
||||
requests: int
|
||||
saved_rows: int
|
||||
oldest_kst: datetime | None
|
||||
newest_kst: datetime | None
|
||||
reached_target: bool
|
||||
|
||||
|
||||
class CandleDownloader:
|
||||
"""지정 기간 캔들을 수집해 SQLite에 저장한다."""
|
||||
|
||||
def __init__(self, settings: Settings, client: BithumbCandleClient | None = None) -> None:
|
||||
"""다운로더를 초기화한다.
|
||||
|
||||
Args:
|
||||
settings: 애플리케이션 설정.
|
||||
client: API 클라이언트. None이면 기본 생성.
|
||||
"""
|
||||
self.settings = settings
|
||||
self.client = client or BithumbCandleClient(
|
||||
base_url=settings.api_url,
|
||||
count=settings.candle_count,
|
||||
sleep_sec=settings.request_sleep_sec,
|
||||
retries=settings.request_retries,
|
||||
)
|
||||
|
||||
def download_interval(
|
||||
self,
|
||||
store: CandleStore,
|
||||
interval_min: int,
|
||||
days: int | None = None,
|
||||
) -> DownloadResult:
|
||||
"""한 인터벌의 캔들을 역방향으로 수집한다.
|
||||
|
||||
Args:
|
||||
store: SQLite 저장소.
|
||||
interval_min: 분 단위 (1440=일봉).
|
||||
days: 수집 일수. None이면 settings.download_days.
|
||||
|
||||
Returns:
|
||||
DownloadResult.
|
||||
"""
|
||||
lookback_days = days if days is not None else self.settings.download_days
|
||||
target_start = datetime.now() - timedelta(days=lookback_days)
|
||||
table = store.ensure_table(self.settings.symbol, interval_min)
|
||||
|
||||
to_kst: datetime | None = None
|
||||
requests = 0
|
||||
saved_rows = 0
|
||||
oldest_kst: datetime | None = None
|
||||
newest_kst: datetime | None = None
|
||||
reached_target = False
|
||||
seen_oldest: set[str] = set()
|
||||
|
||||
logger.info(
|
||||
"수집 시작: %s %s, 목표=%s 이후",
|
||||
self.settings.market,
|
||||
interval_label(interval_min),
|
||||
target_start.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
|
||||
while True:
|
||||
batch = self.client.fetch_candles(
|
||||
market=self.settings.market,
|
||||
interval_min=interval_min,
|
||||
to_kst=to_kst,
|
||||
)
|
||||
requests += 1
|
||||
|
||||
if not batch:
|
||||
logger.info("%s: 더 이상 데이터 없음", interval_label(interval_min))
|
||||
break
|
||||
|
||||
rows = [
|
||||
candle_to_row(c, self.settings.symbol, self.settings.coin_name)
|
||||
for c in batch
|
||||
]
|
||||
saved_rows += store.upsert_rows(table, rows)
|
||||
|
||||
batch_oldest = parse_kst_datetime(batch[-1]["candle_date_time_kst"])
|
||||
batch_newest = parse_kst_datetime(batch[0]["candle_date_time_kst"])
|
||||
|
||||
if newest_kst is None or batch_newest > newest_kst:
|
||||
newest_kst = batch_newest
|
||||
if oldest_kst is None or batch_oldest < oldest_kst:
|
||||
oldest_kst = batch_oldest
|
||||
|
||||
if batch_oldest <= target_start:
|
||||
reached_target = True
|
||||
logger.info(
|
||||
"%s: 목표 기간 도달 (oldest=%s)",
|
||||
interval_label(interval_min),
|
||||
batch_oldest.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
break
|
||||
|
||||
oldest_key = batch[-1]["candle_date_time_kst"]
|
||||
if oldest_key in seen_oldest:
|
||||
logger.warning("%s: 페이지네이션 정체 — 중단", interval_label(interval_min))
|
||||
break
|
||||
seen_oldest.add(oldest_key)
|
||||
|
||||
to_kst = batch_oldest
|
||||
|
||||
if requests % 20 == 0:
|
||||
logger.info(
|
||||
"%s: 진행 중 requests=%s saved=%s oldest=%s",
|
||||
interval_label(interval_min),
|
||||
requests,
|
||||
saved_rows,
|
||||
batch_oldest.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
|
||||
return DownloadResult(
|
||||
interval_min=interval_min,
|
||||
requests=requests,
|
||||
saved_rows=saved_rows,
|
||||
oldest_kst=oldest_kst,
|
||||
newest_kst=newest_kst,
|
||||
reached_target=reached_target,
|
||||
)
|
||||
|
||||
def download_all(self, store: CandleStore, days: int | None = None) -> list[DownloadResult]:
|
||||
"""설정된 모든 인터벌을 수집한다.
|
||||
|
||||
Args:
|
||||
store: SQLite 저장소.
|
||||
days: 수집 일수.
|
||||
|
||||
Returns:
|
||||
인터벌별 DownloadResult 리스트.
|
||||
"""
|
||||
results: list[DownloadResult] = []
|
||||
for interval in self.settings.download_intervals:
|
||||
if interval == 1:
|
||||
logger.warning("1분봉은 장기 수집 시 요청량이 매우 큽니다 — 건너뜁니다.")
|
||||
continue
|
||||
result = self.download_interval(store, interval, days=days)
|
||||
results.append(result)
|
||||
return results
|
||||
Reference in New Issue
Block a user