feat(vol_breakout): 15m 현물 롱 라이브·모니터·cron 운영 추가
vol_breakout 멀티종목 tick, vol_live HTML 모니터, 마감 봉만 저장하는 캔들 다운로드, 텔레그램 체결 알림, cron/watch 감시 스크립트 및 테스트를 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,7 +16,7 @@ if str(SRC) not in sys.path:
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.config import load_settings, resolve_coin_name
|
||||
from bithumb.data.candle_store import CandleStore
|
||||
from bithumb.data.downloader import CandleDownloader
|
||||
from bithumb.data.intervals import INTERVAL_1MIN, estimate_download_requests, interval_label
|
||||
@@ -32,6 +32,58 @@ def _configure_logging(verbose: bool) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _parse_symbols(raw: str | None, defaults: list[str]) -> list[str]:
|
||||
"""CLI --symbols 또는 기본 DOWNLOAD_SYMBOLS 목록."""
|
||||
if raw:
|
||||
return [part.strip().upper() for part in raw.split(",") if part.strip()]
|
||||
return list(defaults)
|
||||
|
||||
|
||||
def _log_interval_estimates(
|
||||
*,
|
||||
store: CandleStore,
|
||||
symbol: str,
|
||||
intervals: list[int],
|
||||
days: int,
|
||||
full: bool,
|
||||
batch_size: int,
|
||||
sleep_sec: float,
|
||||
log: logging.Logger,
|
||||
) -> None:
|
||||
"""인터벌별 예상 API 요청 수를 로깅한다."""
|
||||
for interval in intervals:
|
||||
if full:
|
||||
est = estimate_download_requests(interval, days, batch_size=batch_size)
|
||||
log.info(
|
||||
"예상 API 요청: %s %s ≈ %s회 (풀 다운, sleep %.2fs)",
|
||||
symbol,
|
||||
interval_label(interval),
|
||||
est,
|
||||
sleep_sec,
|
||||
)
|
||||
continue
|
||||
_, _, db_max = store.get_range(symbol, interval)
|
||||
if db_max is None:
|
||||
est = estimate_download_requests(interval, days, batch_size=batch_size)
|
||||
log.info(
|
||||
"예상 API 요청: %s %s ≈ %s회 (DB 없음 → 풀 다운)",
|
||||
symbol,
|
||||
interval_label(interval),
|
||||
est,
|
||||
)
|
||||
else:
|
||||
gap_days = max(1, (datetime.now() - db_max).days + 1)
|
||||
est = estimate_download_requests(interval, gap_days, batch_size=batch_size)
|
||||
log.info(
|
||||
"예상 API 요청: %s %s ≈ %s회 (증분, DB=%s, 갭≈%s일)",
|
||||
symbol,
|
||||
interval_label(interval),
|
||||
est,
|
||||
db_max.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
gap_days,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""CLI 진입점."""
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -54,6 +106,12 @@ def main() -> int:
|
||||
default=None,
|
||||
help="(고급) 쉼표 구분 인터벌만 수집. 기본: .env DOWNLOAD_INTERVALS 전체",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--symbols",
|
||||
type=str,
|
||||
default=None,
|
||||
help="쉼표 구분 심볼 (기본: .env DOWNLOAD_SYMBOLS 또는 SYMBOL)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-1min",
|
||||
action="store_true",
|
||||
@@ -63,8 +121,10 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
|
||||
_configure_logging(args.verbose)
|
||||
settings = load_settings()
|
||||
base_settings = load_settings()
|
||||
symbols = _parse_symbols(args.symbols, base_settings.download_symbols)
|
||||
|
||||
settings = base_settings
|
||||
if args.intervals:
|
||||
settings = replace(
|
||||
settings,
|
||||
@@ -83,76 +143,59 @@ def main() -> int:
|
||||
log = logging.getLogger(__name__)
|
||||
log.info(
|
||||
"대상=%s DB=%s mode=%s days=%s intervals=%s",
|
||||
settings.market,
|
||||
",".join(f"KRW-{s}" for s in symbols),
|
||||
settings.db_path,
|
||||
mode_label,
|
||||
days,
|
||||
settings.download_intervals,
|
||||
)
|
||||
for interval in settings.download_intervals:
|
||||
est = estimate_download_requests(interval, days, batch_size=settings.candle_count)
|
||||
log.info(
|
||||
"예상 API 요청: %s ≈ %s회 (sleep %.2fs)",
|
||||
interval_label(interval),
|
||||
est,
|
||||
settings.request_sleep_sec,
|
||||
)
|
||||
|
||||
store = CandleStore(settings.db_path)
|
||||
exit_code = 0
|
||||
try:
|
||||
for interval in settings.download_intervals:
|
||||
if args.full:
|
||||
est = estimate_download_requests(interval, days, batch_size=settings.candle_count)
|
||||
log.info(
|
||||
"예상 API 요청: %s ≈ %s회 (풀 다운, sleep %.2fs)",
|
||||
interval_label(interval),
|
||||
est,
|
||||
settings.request_sleep_sec,
|
||||
)
|
||||
else:
|
||||
_, _, db_max = store.get_range(settings.symbol, interval)
|
||||
if db_max is None:
|
||||
est = estimate_download_requests(interval, days, batch_size=settings.candle_count)
|
||||
log.info(
|
||||
"예상 API 요청: %s ≈ %s회 (DB 없음 → 풀 다운)",
|
||||
interval_label(interval),
|
||||
est,
|
||||
)
|
||||
else:
|
||||
gap_days = max(1, (datetime.now() - db_max).days + 1)
|
||||
est = estimate_download_requests(interval, gap_days, batch_size=settings.candle_count)
|
||||
log.info(
|
||||
"예상 API 요청: %s ≈ %s회 (증분, DB=%s, 갭≈%s일)",
|
||||
interval_label(interval),
|
||||
est,
|
||||
db_max.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
gap_days,
|
||||
)
|
||||
|
||||
downloader = CandleDownloader(settings)
|
||||
results = downloader.download_all(store, days=days, full=args.full)
|
||||
|
||||
print(f"\n=== 수집 완료 ({mode_label}) ===")
|
||||
for result in results:
|
||||
count, min_dt, max_dt = store.get_range(settings.symbol, result.interval_min)
|
||||
min_s = min_dt.strftime("%Y-%m-%d %H:%M:%S") if min_dt else "-"
|
||||
max_s = max_dt.strftime("%Y-%m-%d %H:%M:%S") if max_dt else "-"
|
||||
if result.mode == "uptodate":
|
||||
flag = "UPTODATE"
|
||||
elif result.reached_target:
|
||||
flag = "OK"
|
||||
else:
|
||||
flag = "PARTIAL"
|
||||
label = interval_label(result.interval_min)
|
||||
print(
|
||||
f"[{flag}] {label} ({result.interval_min}) mode={result.mode} | "
|
||||
f"requests={result.requests} upsert={result.saved_rows} "
|
||||
f"db_rows={count} range={min_s} ~ {max_s}"
|
||||
for symbol in symbols:
|
||||
symbol_settings = replace(
|
||||
settings,
|
||||
symbol=symbol,
|
||||
coin_name=resolve_coin_name(symbol),
|
||||
)
|
||||
print(f"\n=== {symbol} ({symbol_settings.coin_name}) ===")
|
||||
_log_interval_estimates(
|
||||
store=store,
|
||||
symbol=symbol,
|
||||
intervals=symbol_settings.download_intervals,
|
||||
days=days,
|
||||
full=args.full,
|
||||
batch_size=symbol_settings.candle_count,
|
||||
sleep_sec=symbol_settings.request_sleep_sec,
|
||||
log=log,
|
||||
)
|
||||
|
||||
downloader = CandleDownloader(symbol_settings)
|
||||
results = downloader.download_all(store, days=days, full=args.full)
|
||||
|
||||
print(f"\n--- {symbol} 수집 완료 ({mode_label}) ---")
|
||||
for result in results:
|
||||
count, min_dt, max_dt = store.get_range(symbol, result.interval_min)
|
||||
min_s = min_dt.strftime("%Y-%m-%d %H:%M:%S") if min_dt else "-"
|
||||
max_s = max_dt.strftime("%Y-%m-%d %H:%M:%S") if max_dt else "-"
|
||||
if result.mode == "uptodate":
|
||||
flag = "UPTODATE"
|
||||
elif result.reached_target:
|
||||
flag = "OK"
|
||||
else:
|
||||
flag = "PARTIAL"
|
||||
exit_code = 1
|
||||
label = interval_label(result.interval_min)
|
||||
print(
|
||||
f"[{flag}] {symbol} {label} ({result.interval_min}) mode={result.mode} | "
|
||||
f"requests={result.requests} upsert={result.saved_rows} "
|
||||
f"db_rows={count} range={min_s} ~ {max_s}"
|
||||
)
|
||||
finally:
|
||||
store.close()
|
||||
|
||||
return 0
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user