140 lines
4.4 KiB
Python
140 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import timedelta
|
|
from pathlib import Path
|
|
|
|
from crypto_spot_bot.models import Signal, utc_now
|
|
from crypto_spot_bot.storage import MAX_SIGNAL_DIAGNOSTICS_BYTES, PRUNE_BATCH_SIZE, Storage
|
|
from tools.compact_runtime_db import compact_database
|
|
|
|
|
|
def test_hold_sampling_is_independent_for_each_reason_and_diagnostics_are_bounded(tmp_path) -> None:
|
|
storage = Storage(tmp_path / "tradebot.sqlite3")
|
|
diagnostics = {
|
|
"strategy_mode": "torch_forecast",
|
|
"checks": {"model_fresh_ok": False},
|
|
"forecast": {
|
|
"model": "torch_lstm",
|
|
"expected_return_percent": 0.42,
|
|
"model_fresh": False,
|
|
"feature_snapshot": [
|
|
{"name": f"feature-{index}", "interpretation": "x" * 1000}
|
|
for index in range(100)
|
|
],
|
|
},
|
|
}
|
|
|
|
first = Signal("BTCUSDT", "HOLD", 0.2, "entry blocked", diagnostics)
|
|
second = Signal("BTCUSDT", "HOLD", 0.2, "position held", diagnostics)
|
|
|
|
assert storage.insert_signal(first, hold_sample_seconds=60) is True
|
|
assert storage.insert_signal(second, hold_sample_seconds=60) is True
|
|
assert storage.insert_signal(first, hold_sample_seconds=60) is False
|
|
|
|
rows = storage.recent_signals(10)
|
|
assert len(rows) == 2
|
|
stored = json.loads(rows[0]["diagnostics_json"])
|
|
assert len(rows[0]["diagnostics_json"].encode("utf-8")) <= MAX_SIGNAL_DIAGNOSTICS_BYTES
|
|
assert stored["forecast"]["model"] == "torch_lstm"
|
|
assert "feature_snapshot" not in stored["forecast"]
|
|
|
|
|
|
def test_prune_deletes_only_one_bounded_batch_per_table(tmp_path) -> None:
|
|
storage = Storage(tmp_path / "tradebot.sqlite3")
|
|
old_timestamp = (utc_now() - timedelta(days=90)).isoformat()
|
|
rows = [
|
|
("BTCUSDT", "HOLD", 0.0, "old", "{}", old_timestamp)
|
|
for _ in range(PRUNE_BATCH_SIZE + 5)
|
|
]
|
|
with storage.connect() as conn:
|
|
conn.executemany(
|
|
"""
|
|
INSERT INTO signals (symbol, action, confidence, reason, diagnostics_json, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
rows,
|
|
)
|
|
|
|
deleted = storage.prune(30)
|
|
|
|
assert deleted["signals"] == PRUNE_BATCH_SIZE
|
|
assert len(storage.recent_signals(PRUNE_BATCH_SIZE + 10)) == 5
|
|
|
|
|
|
def test_market_observation_export_is_symbol_scoped_and_paginated(tmp_path) -> None:
|
|
storage = Storage(tmp_path / "tradebot.sqlite3")
|
|
first_id = storage.insert_market_observation(
|
|
symbol="BTCUSDT",
|
|
bid_price=100.0,
|
|
bid_size=2.0,
|
|
ask_price=101.0,
|
|
ask_size=1.0,
|
|
mid_price=100.5,
|
|
microprice=100.6666666667,
|
|
spread_bps=99.50248756,
|
|
imbalance=1 / 3,
|
|
last_price=100.4,
|
|
source_timestamp_ms=1_789_000_000_000,
|
|
)
|
|
second_id = storage.insert_market_observation(
|
|
symbol="BTCUSDT",
|
|
bid_price=101.0,
|
|
bid_size=1.0,
|
|
ask_price=102.0,
|
|
ask_size=1.0,
|
|
mid_price=101.5,
|
|
microprice=101.5,
|
|
spread_bps=98.52216749,
|
|
imbalance=0.0,
|
|
last_price=101.4,
|
|
source_timestamp_ms=1_789_000_030_000,
|
|
)
|
|
storage.insert_market_observation(
|
|
symbol="ETHUSDT",
|
|
bid_price=10.0,
|
|
bid_size=1.0,
|
|
ask_price=11.0,
|
|
ask_size=1.0,
|
|
mid_price=10.5,
|
|
microprice=10.5,
|
|
spread_bps=952.38095238,
|
|
imbalance=0.0,
|
|
last_price=10.4,
|
|
)
|
|
|
|
rows = storage.market_observations_after(
|
|
symbol="BTCUSDT",
|
|
after_id=first_id,
|
|
limit=1,
|
|
)
|
|
|
|
assert [row["id"] for row in rows] == [second_id]
|
|
assert rows[0]["source_timestamp_ms"] == 1_789_000_030_000
|
|
|
|
|
|
def test_runtime_compaction_preserves_durable_state_and_bounds_telemetry(tmp_path) -> None:
|
|
database = tmp_path / "tradebot.sqlite3"
|
|
storage = Storage(database)
|
|
for index in range(10):
|
|
storage.insert_signal(
|
|
Signal("BTCUSDT", "BUY", 0.8, f"signal-{index}"),
|
|
hold_sample_seconds=0,
|
|
)
|
|
storage.set_runtime("active", {"value": 1})
|
|
|
|
result = compact_database(
|
|
database,
|
|
recent_rows={"signals": 3, "equity": 0, "events": 0, "llm_advice": 0},
|
|
)
|
|
|
|
compacted = Storage(database)
|
|
assert [row["reason"] for row in compacted.recent_signals(10)] == [
|
|
"signal-9",
|
|
"signal-8",
|
|
"signal-7",
|
|
]
|
|
assert compacted.get_runtime("active") == {"value": 1}
|
|
assert Path(result["backup"]).is_file()
|
|
assert result["rows"]["signals"] == 3
|