"""캔들 봉 마감·다운로드 필터.""" from __future__ import annotations from datetime import datetime, timedelta from bithumb.data.candle_bars import ( is_closed_candle, last_closed_bar_open, only_closed_candles, ) def test_is_closed_candle_15m() -> None: """15분봉은 마감 시각 이후에만 closed.""" bar_open = datetime(2026, 6, 27, 16, 15, 0) assert not is_closed_candle( bar_open, 15, now=datetime(2026, 6, 27, 16, 29, 59) ) assert is_closed_candle( bar_open, 15, now=datetime(2026, 6, 27, 16, 30, 0) ) def test_only_closed_candles_filters_forming_bar() -> None: """API 배치에서 진행 중 봉은 제외.""" rows = [ ("2026-06-27 16:00:00", 100.0, 110.0, 95.0, 105.0, 1.0), ("2026-06-27 16:15:00", 105.0, 105.0, 105.0, 105.0, 0.1), ] now = datetime(2026, 6, 27, 16, 20, 0) closed = only_closed_candles(rows, 15, now=now) assert len(closed) == 1 assert closed[0][0] == "2026-06-27 16:00:00" def test_last_closed_bar_open() -> None: """진행 중인 16:15~16:30 봉 기준 최근 마감은 16:00.""" now = datetime(2026, 6, 27, 16, 20, 0) assert last_closed_bar_open(now, 15) == datetime(2026, 6, 27, 16, 0, 0) now = datetime(2026, 6, 27, 16, 30, 0) assert last_closed_bar_open(now, 15) == datetime(2026, 6, 27, 16, 15, 0) def test_only_closed_candles_keeps_all_historical() -> None: """과거 봉은 모두 마감으로 간주.""" rows = [ ("2026-06-26 20:00:00", 1.0, 2.0, 0.5, 1.5, 10.0), ("2026-06-26 20:15:00", 1.5, 2.5, 1.0, 2.0, 8.0), ] now = datetime(2026, 6, 27, 22, 0, 0) assert len(only_closed_candles(rows, 15, now=now)) == 2 def test_delete_incomplete_tail(tmp_path) -> None: """미마감 최신 봉이 DB에서 제거된다.""" from bithumb.data.candle_store import CandleStore db = tmp_path / "t.db" store = CandleStore(db) rows = [ ("2026-06-27 16:00:00", 100.0, 110.0, 95.0, 105.0, 1.0), ("2026-06-27 16:15:00", 105.0, 105.0, 105.0, 105.0, 0.1), ] store.upsert_rows("NEAR", "NEAR", 15, rows) deleted = store.delete_incomplete_tail( "NEAR", 15, now=datetime(2026, 6, 27, 16, 20, 0) ) assert deleted == 1 _, _, db_max = store.get_range("NEAR", 15) assert db_max == datetime(2026, 6, 27, 16, 0, 0) store.close()