Team Ai
Modelpublic

ParallelLLC/algorithmic_trading

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
27likes32downloads
test_yahoo_data_stream.py230 linesDownload Raw Back to tests
1import logging2 3import pandas as pd4import pytest5from unittest.mock import patch6 7from agentic_ai_system.yahoo_data_stream import YahooDataStream8 9 10@pytest.fixture11def yahoo_config():12    return {13        'data_source': {'type': 'yahoo'},14        'yahoo': {'poll_interval_seconds': 1, 'auto_adjust': False},15        'trading': {16            'symbol': 'AAPL',17            'timeframe': '1d',18        },19        'realtime_data': {'buffer_size': 10},20    }21 22 23def _sample_yahoo_frame():24    idx = pd.date_range('2024-06-03', periods=3, freq='D', tz='America/New_York')25    return pd.DataFrame(26        {27            'Open': [190.0, 191.0, 192.0],28            'High': [191.5, 192.5, 193.5],29            'Low': [189.0, 190.0, 191.0],30            'Close': [191.0, 192.0, 193.0],31            'Volume': [1_000_000, 1_100_000, 1_200_000],32        },33        index=idx,34    )35 36 37def _split_frame():38    """An unadjusted 10:1 split, as Yahoo returns it with auto_adjust=False.39 40    Modelled on NVDA, 10 June 2024: the raw Close drops from ~1200 to ~120 and41    a backtest reads it as a -90% day.42    """43    idx = pd.date_range('2024-06-06', periods=4, freq='D', tz='America/New_York')44    close = [1200.0, 1208.0, 120.5, 121.0]45    return pd.DataFrame(46        {47            'Open': close,48            'High': [c * 1.01 for c in close],49            'Low': [c * 0.99 for c in close],50            'Close': close,51            'Volume': [1_000_000] * 4,52        },53        index=idx,54    )55 56 57def _dated_frame(timestamps, close=100.0):58    return pd.DataFrame(59        {60            'timestamp': [pd.Timestamp(t) for t in timestamps],61            'open': close,62            'high': close,63            'low': close,64            'close': close,65            'volume': 1_000.0,66        }67    )68 69 70class TestYahooDataStream:71    def test_initialization_from_symbol(self, yahoo_config):72        stream = YahooDataStream(yahoo_config)73        assert stream.symbols == ['AAPL']74        assert stream.interval == '1d'75 76    def test_normalize_ohlcv(self, yahoo_config):77        stream = YahooDataStream(yahoo_config)78        df = stream._normalize_ohlcv(_sample_yahoo_frame())79        assert list(df.columns) == ['timestamp', 'open', 'high', 'low', 'close', 'volume']80        assert len(df) == 381        assert df['close'].iloc[-1] == 193.082 83    def test_clamp_intraday_lookback(self, yahoo_config):84        yahoo_config['trading']['timeframe'] = '1m'85        stream = YahooDataStream(yahoo_config)86        start, end = stream._clamp_window('2020-01-01', '2026-01-01', '1m')87        assert start > '2020-01-01'88        assert end >= start89 90    def test_get_historical_data(self, yahoo_config):91        stream = YahooDataStream(yahoo_config)92        with patch.object(stream, '_download', return_value=_sample_yahoo_frame()):93            df = stream.get_historical_data('AAPL', '2024-01-01', '2024-12-31')94        assert len(df) == 395        assert 'open' in df.columns96 97 98class TestPriceAdjustment:99    """Unadjusted prices turn every split into a phantom crash."""100 101    def test_adjustment_is_on_by_default(self):102        stream = YahooDataStream({'trading': {'symbol': 'AAPL', 'timeframe': '1d'}})103        assert stream.auto_adjust is True104 105    def test_auto_adjust_is_passed_through_to_yfinance(self):106        stream = YahooDataStream({'trading': {'symbol': 'AAPL', 'timeframe': '1d'}})107        with patch('yfinance.download', return_value=_sample_yahoo_frame()) as download:108            stream._download('AAPL', period='5d', interval='1d')109        assert download.call_args.kwargs['auto_adjust'] is True110 111    def test_opting_out_of_adjustment_warns(self, caplog):112        with caplog.at_level(logging.WARNING):113            YahooDataStream({114                'trading': {'symbol': 'AAPL', 'timeframe': '1d'},115                'yahoo': {'auto_adjust': False},116            })117        assert any('not split' in r.message.lower() for r in caplog.records)118 119    def test_split_sized_move_is_flagged(self, yahoo_config, caplog):120        stream = YahooDataStream(yahoo_config)121        df = stream._normalize_ohlcv(_split_frame())122        with caplog.at_level(logging.WARNING):123            found = stream._warn_if_unadjusted('NVDA', df)124        assert found == 1125        assert any('auto_adjust' in r.message for r in caplog.records)126 127    def test_ordinary_moves_are_not_flagged(self, yahoo_config, caplog):128        stream = YahooDataStream(yahoo_config)129        df = stream._normalize_ohlcv(_sample_yahoo_frame())130        with caplog.at_level(logging.WARNING):131            assert stream._warn_if_unadjusted('AAPL', df) == 0132 133    def test_intraday_bars_are_not_split_checked(self, yahoo_config):134        """A 40% move in one minute is a halt or a fat finger, not a split."""135        yahoo_config['trading']['timeframe'] = '1m'136        stream = YahooDataStream(yahoo_config)137        df = stream._normalize_ohlcv(_split_frame())138        assert stream._warn_if_unadjusted('NVDA', df) == 0139 140 141class TestIncompleteBars:142    """The bar Yahoo is still building must not be reported as final."""143 144    def test_forming_bar_is_dropped(self, yahoo_config):145        stream = YahooDataStream(yahoo_config)146        now = pd.Timestamp.now(tz='UTC').tz_convert(None).normalize()147        df = _dated_frame([now - pd.Timedelta(days=2), now - pd.Timedelta(days=1), now])148        kept = stream._drop_incomplete(df)149        assert len(kept) == 2150        assert kept['timestamp'].max() < now151 152    def test_finished_bars_all_survive(self, yahoo_config):153        stream = YahooDataStream(yahoo_config)154        now = pd.Timestamp.now(tz='UTC').tz_convert(None).normalize()155        df = _dated_frame([now - pd.Timedelta(days=5), now - pd.Timedelta(days=4)])156        assert len(stream._drop_incomplete(df)) == 2157 158    def test_opting_in_keeps_the_forming_bar(self, yahoo_config):159        yahoo_config['yahoo']['emit_incomplete_bars'] = True160        stream = YahooDataStream(yahoo_config)161        now = pd.Timestamp.now(tz='UTC').tz_convert(None).normalize()162        df = _dated_frame([now - pd.Timedelta(days=1), now])163        assert len(stream._drop_incomplete(df)) == 2164 165    def test_partial_bar_is_never_emitted_then_stranded(self, yahoo_config):166        """The bug this guards: emitting the forming bar advanced the watermark,167        so the finished version of that same bar never reached a callback."""168        stream = YahooDataStream(yahoo_config)169        received = []170        stream.add_data_callback(lambda kind, bar: received.append(bar))171 172        now = pd.Timestamp.now(tz='UTC').tz_convert(None).normalize()173        yesterday, today = now - pd.Timedelta(days=1), now174 175        stream._ingest_new_bars('AAPL', _dated_frame([yesterday, today], close=100.0))176        assert len(received) == 1  # only yesterday's completed bar177 178        # Next day: what was the forming bar is now final and must arrive.179        with patch.object(stream, '_drop_incomplete', side_effect=lambda d: d):180            stream._ingest_new_bars('AAPL', _dated_frame([yesterday, today], close=105.0))181        assert len(received) == 2182        assert received[-1]['close'] == 105.0183 184 185class TestPollBackoff:186    """Yahoo rate-limits hard, and a fixed interval keeps you throttled."""187 188    def test_success_polls_at_the_configured_interval(self, yahoo_config):189        yahoo_config['yahoo']['poll_interval_seconds'] = 60190        stream = YahooDataStream(yahoo_config)191        stream._consecutive_failures = 0192        assert 48 <= stream._next_delay() <= 72  # 60s +/- jitter193 194    def test_delay_grows_with_consecutive_failures(self, yahoo_config):195        yahoo_config['yahoo']['poll_interval_seconds'] = 60196        stream = YahooDataStream(yahoo_config)197        delays = []198        for failures in (1, 2, 3):199            stream._consecutive_failures = failures200            delays.append(stream._next_delay())201        assert delays[0] < delays[1] < delays[2]202 203    def test_backoff_is_capped(self, yahoo_config):204        yahoo_config['yahoo']['poll_interval_seconds'] = 60205        yahoo_config['yahoo']['max_backoff_seconds'] = 300206        stream = YahooDataStream(yahoo_config)207        stream._consecutive_failures = 20208        assert stream._next_delay() <= 300 * 1.2209 210    def test_jitter_desynchronises_retries(self, yahoo_config):211        stream = YahooDataStream(yahoo_config)212        stream._consecutive_failures = 3213        assert len({stream._next_delay() for _ in range(20)}) > 1214 215    def test_poll_reports_failure_when_every_symbol_fails(self, yahoo_config):216        stream = YahooDataStream(yahoo_config)217        with patch.object(stream, '_download', side_effect=RuntimeError('429 Too Many Requests')):218            assert stream._poll_once() is False219 220    def test_poll_reports_success_when_a_symbol_returns_bars(self, yahoo_config):221        stream = YahooDataStream(yahoo_config)222        with patch.object(stream, '_download', return_value=_sample_yahoo_frame()):223            assert stream._poll_once() is True224 225    def test_empty_response_counts_as_failure(self, yahoo_config):226        """A rate-limited yfinance returns an empty frame rather than raising."""227        stream = YahooDataStream(yahoo_config)228        with patch.object(stream, '_download', return_value=pd.DataFrame()):229            assert stream._poll_once() is False230