1+ """Binance vendor: native OHLCV data for crypto assets.
2+
3+ Covers symbol resolution, date-boundary inclusivity (#binance-pagination),
4+ pagination past the 1000-candle-per-request cap, and the
5+ NoMarketDataError contract for unrecognized symbols / empty responses.
6+ """
7+ import pytest
8+
9+ import tradingagents .dataflows .binance as binance
10+
11+
12+ class _FakeResponse :
13+ def __init__ (self , payload ):
14+ self ._payload = payload
15+
16+ def raise_for_status (self ):
17+ pass
18+
19+ def json (self ):
20+ return self ._payload
21+
22+
23+ def _kline_row (date_str , price = 50000.0 , volume = 100.0 ):
24+ """Build one Binance-shaped kline row for a given date."""
25+ import pandas as pd
26+ open_ms = int (pd .Timestamp (date_str , tz = "UTC" ).timestamp () * 1000 )
27+ return [
28+ open_ms , str (price ), str (price + 10 ), str (price - 10 ), str (price + 5 ),
29+ str (volume ), open_ms + 86399999 , "0" , 1 , "0" , "0" , "0" ,
30+ ]
31+
32+
33+ @pytest .mark .unit
34+ def test_unrecognized_symbol_raises_no_market_data_error ():
35+ with pytest .raises (binance .NoMarketDataError ):
36+ binance .get_binance_stock ("NOTACOIN-USD" , "2026-06-01" , "2026-06-10" )
37+
38+
39+ @pytest .mark .unit
40+ def test_empty_response_raises_no_market_data_error (monkeypatch ):
41+ monkeypatch .setattr (
42+ binance .requests , "get" ,
43+ lambda * a , ** k : _FakeResponse ([]),
44+ )
45+ with pytest .raises (binance .NoMarketDataError ):
46+ binance .get_binance_stock ("BTC-USD" , "2026-06-01" , "2026-06-10" )
47+
48+
49+ @pytest .mark .unit
50+ def test_normal_path_returns_expected_format (monkeypatch ):
51+ rows = [_kline_row (d ) for d in
52+ ["2026-06-01" , "2026-06-02" , "2026-06-03" ]]
53+ monkeypatch .setattr (
54+ binance .requests , "get" ,
55+ lambda * a , ** k : _FakeResponse (rows ),
56+ )
57+ result = binance .get_binance_stock ("BTC-USD" , "2026-06-01" , "2026-06-03" )
58+ assert "Total records: 3" in result
59+ assert "2026-06-03" in result # end_date must be included
60+ assert "BTCUSDT" in result
61+
62+
63+ @pytest .mark .unit
64+ def test_end_date_inclusive_even_at_batch_boundary (monkeypatch ):
65+ # Simulate Binance returning one extra day past end_date (the buffered
66+ # request); the vendor must locally filter it out.
67+ rows = [_kline_row (d ) for d in
68+ ["2026-06-01" , "2026-06-02" , "2026-06-03" ]] # 06-03 is the buffer day
69+ monkeypatch .setattr (
70+ binance .requests , "get" ,
71+ lambda * a , ** k : _FakeResponse (rows ),
72+ )
73+ result = binance .get_binance_stock ("BTC-USD" , "2026-06-01" , "2026-06-02" )
74+ assert "Total records: 2" in result
75+ assert "2026-06-03" not in result # buffer day must be filtered out
76+
77+
78+ @pytest .mark .unit
79+ def test_pagination_stops_when_batch_smaller_than_limit (monkeypatch ):
80+ calls = []
81+
82+ def fake_get (url , params = None , ** kwargs ):
83+ calls .append (params )
84+ # First call: full page (simulated as MAX_LIMIT rows) triggers a
85+ # second call; second call returns fewer rows, ending pagination.
86+ if len (calls ) == 1 :
87+ return _FakeResponse (
88+ [_kline_row (f"2020-01-{ d :02d} " ) for d in range (1 , 32 )]
89+ * (binance ._MAX_LIMIT // 31 + 1 )
90+ )
91+ return _FakeResponse ([_kline_row ("2026-06-01" )])
92+
93+ monkeypatch .setattr (binance .requests , "get" , fake_get )
94+ binance .get_binance_stock ("BTC-USD" , "2020-01-01" , "2026-06-01" )
95+ assert len (calls ) == 2 # confirms pagination actually looped
96+
97+
98+ @pytest .mark .unit
99+ def test_symbol_resolution_uses_usdt_pair (monkeypatch ):
100+ captured = {}
101+
102+ def fake_get (url , params = None , ** kwargs ):
103+ captured .update (params )
104+ return _FakeResponse ([_kline_row ("2026-06-01" )])
105+
106+ monkeypatch .setattr (binance .requests , "get" , fake_get )
107+ binance .get_binance_stock ("BTC-USD" , "2026-06-01" , "2026-06-01" )
108+ assert captured ["symbol" ] == "BTCUSDT"
0 commit comments