feat: collect Bybit orderbook observations for training
This commit is contained in:
@@ -125,3 +125,14 @@ def test_websocket_subscribe_uses_configured_kline_interval() -> None:
|
||||
|
||||
assert "kline.60.BTCUSDT" in payload
|
||||
assert "kline.1.BTCUSDT" not in payload
|
||||
|
||||
|
||||
def test_orderbook_level_one_preserves_sizes(make_settings, tmp_path) -> None:
|
||||
client = BybitClient(make_settings(tmp_path))
|
||||
client.public_get = lambda *_args, **_kwargs: {
|
||||
"b": [["100.5", "2.25"]],
|
||||
"a": [["100.7", "1.75"]],
|
||||
}
|
||||
|
||||
assert client.orderbook_level_one("BTCUSDT") == (100.5, 2.25, 100.7, 1.75)
|
||||
assert client.orderbook_top("BTCUSDT") == (100.5, 100.7)
|
||||
|
||||
@@ -180,3 +180,12 @@ def test_load_settings_rejects_unknown_fallback_mode(tmp_path, monkeypatch) -> N
|
||||
|
||||
with pytest.raises(ValueError, match="TIME_SERIES_FALLBACK_MODE"):
|
||||
load_settings(env_file)
|
||||
|
||||
|
||||
def test_load_settings_rejects_non_positive_observation_interval(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.delenv("MARKET_OBSERVATION_SAMPLE_SECONDS", raising=False)
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text("MARKET_OBSERVATION_SAMPLE_SECONDS=0\n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="MARKET_OBSERVATION_SAMPLE_SECONDS"):
|
||||
load_settings(env_file)
|
||||
|
||||
@@ -46,6 +46,8 @@ def test_safe_config_summarizes_torch_forecast_artifact(make_settings, tmp_path)
|
||||
assert config["time_series_probe_size_multiplier"] == 0.40
|
||||
assert config["time_series_rebound_fallback_enabled"] is True
|
||||
assert config["time_series_fallback_mode"] == "trend_macd"
|
||||
assert config["market_observation_enabled"] is True
|
||||
assert config["market_observation_sample_seconds"] == 30.0
|
||||
assert config["time_series_model_artifact"] == {
|
||||
"available": True,
|
||||
"type": "pytorch_recurrent_forecaster",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from crypto_spot_bot.market_data import _candles_due, _closed_candles, _is_closed_kline_row
|
||||
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:
|
||||
@@ -26,3 +27,37 @@ def test_rest_candles_refresh_only_after_next_bar_closes() -> None:
|
||||
|
||||
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
|
||||
|
||||
@@ -62,6 +62,57 @@ def test_prune_deletes_only_one_bounded_batch_per_table(tmp_path) -> None:
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user