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:
dsyoon
2026-06-29 08:31:14 +09:00
parent 8413bbd536
commit 72de8d534e
58 changed files with 6008 additions and 1286 deletions

143
tests/test_ops_ledger.py Normal file
View File

@@ -0,0 +1,143 @@
"""ledger pending 단위 테스트."""
from __future__ import annotations
from bithumb.operations.runner import (
_advance_cursor_from_ledger,
_apply_backlog_limit,
_is_settled,
_is_signal_api_executable,
_ledger_pending_signals,
_merge_pending_signals,
_settle_expired_backlog,
)
from datetime import datetime
def test_is_settled_executed_or_expected_skip() -> None:
"""executed 또는 expected_skip이면 settled."""
assert _is_settled({"executed": True, "expected_skip": False})
assert _is_settled({"executed": False, "expected_skip": True})
assert not _is_settled({"executed": False, "expected_skip": False})
def test_ledger_pending_skips_settled_and_includes_missing() -> None:
"""history settled 신호는 제외, 미기록 신호는 pending."""
kept = [
{"datetime": "2026-06-14 14:09:00", "side": "buy", "bar_index": 10},
{"datetime": "2026-06-14 14:15:00", "side": "sell", "bar_index": 11},
{"datetime": "2026-06-14 14:51:00", "side": "sell", "bar_index": 12},
]
history = [
{
"datetime": "2026-06-14 14:09:00",
"side": "buy",
"trade": {"executed": True, "expected_skip": False},
},
{
"datetime": "2026-06-14 14:51:00",
"side": "sell",
"trade": {"executed": False, "expected_skip": True},
},
]
pending = _ledger_pending_signals(
kept,
history,
latest_bar_index=12,
lookback_days=3,
)
assert len(pending) == 1
assert pending[0]["datetime"] == "2026-06-14 14:15:00"
def test_ledger_pending_includes_failed_api() -> None:
"""API 실패(expected_skip false)는 재시도 대상."""
kept = [
{"datetime": "2026-06-14 15:00:00", "side": "buy", "bar_index": 20},
]
history = [
{
"datetime": "2026-06-14 15:00:00",
"side": "buy",
"trade": {"executed": False, "expected_skip": False},
},
]
pending = _ledger_pending_signals(
kept,
history,
latest_bar_index=20,
lookback_days=3,
)
assert len(pending) == 1
def test_apply_backlog_limit() -> None:
"""tick당 backlog 상한."""
signals = [
{"datetime": f"2026-06-14 10:{i:02d}:00", "side": "buy", "bar_index": i}
for i in range(5)
]
limited, dropped = _apply_backlog_limit(signals, 2)
assert len(limited) == 2
assert dropped == 3
def test_advance_cursor_from_ledger() -> None:
"""settled 신호까지만 커서 전진."""
kept = [
{"datetime": "2026-06-14 14:09:00", "side": "buy", "bar_index": 10},
{"datetime": "2026-06-14 14:15:00", "side": "sell", "bar_index": 11},
]
history = [
{
"datetime": "2026-06-14 14:09:00",
"side": "buy",
"trade": {"executed": True},
},
]
state: dict = {
"last_processed_datetime": "2026-06-14 13:00:00",
"last_processed_bar_index": 5,
}
_advance_cursor_from_ledger(state, kept, history)
assert state["last_processed_datetime"] == "2026-06-14 14:09:00"
assert state["last_processed_bar_index"] == 10
def test_settle_expired_backlog() -> None:
"""만료 backlog는 API 없이 expected_skip 정산."""
from datetime import timedelta
now = datetime.now()
old_dt = (now - timedelta(hours=2)).strftime("%Y-%m-%d %H:%M:%S")
kept = [{"datetime": old_dt, "side": "buy", "bar_index": 1, "price": 1.0}]
settled = _settle_expired_backlog(
kept,
[],
max_age_minutes=45,
live_since=None,
)
assert len(settled) == 1
assert settled[0]["trade"]["expected_skip"] is True
assert "backlog 만료" in settled[0]["trade"]["skip_reason"]
def test_is_signal_api_executable_recent() -> None:
"""최근 신호는 API 체결 가능."""
from datetime import timedelta
recent = (datetime.now() - timedelta(minutes=10)).strftime("%Y-%m-%d %H:%M:%S")
ok, _ = _is_signal_api_executable(
{"datetime": recent, "side": "buy"},
max_age_minutes=45,
live_since=None,
)
assert ok
def test_merge_pending_dedupes() -> None:
"""ledger·catchup 병합 시 datetime·side 중복 제거."""
a = [{"datetime": "2026-06-14 14:09:00", "side": "buy", "bar_index": 1}]
b = [{"datetime": "2026-06-14 14:09:00", "side": "buy", "bar_index": 1}]
merged = _merge_pending_signals(a, b)
assert len(merged) == 1