from __future__ import annotations from crypto_spot_bot.market_data import MarketData, _candles_due, _closed_candles, _is_closed_kline_row from crypto_spot_bot.models import Candle from crypto_spot_bot.storage import Storage def test_closed_candles_excludes_current_open_interval() -> None: candles = [ Candle(0, 100, 101, 99, 100, 10), Candle(3_600_000, 101, 102, 100, 101, 10), Candle(7_200_000, 102, 103, 101, 102, 10), ] closed = _closed_candles(candles, "60", now_ms=7_200_000 + 1_000) assert [candle.timestamp for candle in closed] == [0, 3_600_000] def test_websocket_kline_requires_confirmed_candle() -> None: assert _is_closed_kline_row({"start": 7_200_000, "confirm": False}, "60") is False assert _is_closed_kline_row({"start": 7_200_000, "confirm": True}, "60") is True def test_rest_candles_refresh_only_after_next_bar_closes() -> None: candle = Candle(10 * 60_000, 1, 1, 1, 1, 1) assert _candles_due([candle], "1", now_ms=11 * 60_000 + 30_000) is False assert _candles_due([candle], "1", now_ms=12 * 60_000) is True def test_orderbook_handler_samples_sizes_and_microstructure(make_settings, tmp_path) -> None: settings = make_settings( tmp_path, market_observation_enabled=True, market_observation_sample_seconds=30.0, ) storage = Storage(settings.database_path) market = MarketData(settings, object(), storage) market._handle_orderbook( "BTCUSDT", {"b": [["100", "3"]], "a": [["101", "1"]]}, source_timestamp_ms=1_789_000_000_000, ) market._handle_orderbook( "BTCUSDT", {"b": [["100", "4"]], "a": [["101", "1"]]}, source_timestamp_ms=1_789_000_001_000, ) metrics = market.orderbook_metrics["BTCUSDT"] rows = storage.market_observations_after(symbol="BTCUSDT") assert metrics["bid_size"] == 4.0 assert metrics["ask_size"] == 1.0 assert metrics["imbalance"] == 0.6 assert metrics["microprice"] == 100.8 assert len(rows) == 1 assert rows[0]["bid_size"] == 3.0 assert rows[0]["ask_size"] == 1.0 assert rows[0]["imbalance"] == 0.5 assert rows[0]["microprice"] == 100.75 assert rows[0]["source_timestamp_ms"] == 1_789_000_000_000