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:
143
tests/test_ops_ledger.py
Normal file
143
tests/test_ops_ledger.py
Normal 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
|
||||
Reference in New Issue
Block a user