vol_breakout 멀티종목 tick, vol_live HTML 모니터, 마감 봉만 저장하는 캔들 다운로드, 텔레그램 체결 알림, cron/watch 감시 스크립트 및 테스트를 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
4.4 KiB
Python
144 lines
4.4 KiB
Python
"""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
|