""" test_live_price_feed.py — Unit tests for live_price_feed module. Tests cover all three implementations (NullPriceFeed, ConstantPriceFeed, NautilusCachePriceFeed) and verify Protocol structural conformance. """ import sys import unittest from unittest.mock import MagicMock sys.path.insert(0, 'nautilus_dolphin') from nautilus_dolphin.nautilus.live_price_feed import ( PriceFeed, NullPriceFeed, ConstantPriceFeed, NautilusCachePriceFeed, ) class TestNullPriceFeed(unittest.TestCase): def setUp(self): self.feed = NullPriceFeed() def test_bid_returns_none(self): self.assertIsNone(self.feed.bid('BTCUSDT')) def test_ask_returns_none(self): self.assertIsNone(self.feed.ask('BTCUSDT')) def test_mid_returns_none(self): self.assertIsNone(self.feed.mid('BTCUSDT')) def test_last_update_ns_returns_zero(self): self.assertEqual(self.feed.last_update_ns('BTCUSDT'), 0) def test_unknown_symbol_returns_none(self): self.assertIsNone(self.feed.bid('UNKNOWNSYM')) def test_satisfies_price_feed_protocol(self): self.assertIsInstance(self.feed, PriceFeed) class TestConstantPriceFeed(unittest.TestCase): def setUp(self): self.feed = ConstantPriceFeed({ 'BTCUSDT': (82000.0, 82010.0), 'ETHUSDT': (1595.5, 1596.0), }) def test_bid_known_symbol(self): self.assertAlmostEqual(self.feed.bid('BTCUSDT'), 82000.0) def test_ask_known_symbol(self): self.assertAlmostEqual(self.feed.ask('BTCUSDT'), 82010.0) def test_mid_known_symbol(self): expected = (82000.0 + 82010.0) / 2.0 self.assertAlmostEqual(self.feed.mid('BTCUSDT'), expected) def test_bid_second_symbol(self): self.assertAlmostEqual(self.feed.bid('ETHUSDT'), 1595.5) def test_ask_second_symbol(self): self.assertAlmostEqual(self.feed.ask('ETHUSDT'), 1596.0) def test_unknown_symbol_bid_returns_none(self): self.assertIsNone(self.feed.bid('SOLUSDT')) def test_unknown_symbol_ask_returns_none(self): self.assertIsNone(self.feed.ask('SOLUSDT')) def test_unknown_symbol_mid_returns_none(self): self.assertIsNone(self.feed.mid('SOLUSDT')) def test_last_update_ns_returns_zero(self): self.assertEqual(self.feed.last_update_ns('BTCUSDT'), 0) def test_update_changes_price(self): self.feed.update('BTCUSDT', 81000.0, 81010.0) self.assertAlmostEqual(self.feed.bid('BTCUSDT'), 81000.0) self.assertAlmostEqual(self.feed.ask('BTCUSDT'), 81010.0) def test_update_new_symbol(self): self.feed.update('SOLUSDT', 120.0, 120.05) self.assertAlmostEqual(self.feed.bid('SOLUSDT'), 120.0) def test_remove_symbol(self): self.feed.remove('BTCUSDT') self.assertIsNone(self.feed.bid('BTCUSDT')) def test_remove_unknown_is_noop(self): # Should not raise self.feed.remove('NONEXISTENT') def test_empty_feed_construction(self): feed = ConstantPriceFeed() self.assertIsNone(feed.bid('BTCUSDT')) def test_satisfies_price_feed_protocol(self): self.assertIsInstance(self.feed, PriceFeed) class TestNautilusCachePriceFeed(unittest.TestCase): def _make_quote_tick(self, bid: float, ask: float, ts_event: int = 1234567890_000_000_000): qt = MagicMock() qt.bid_price = bid qt.ask_price = ask qt.ts_event = ts_event return qt def _make_cache(self, quote_ticks: dict): """ Build a mock NT cache where quote_tick(iid) returns the given tick if iid.value is in quote_ticks, else None. """ cache = MagicMock() def _quote_tick(iid): return quote_ticks.get(str(iid)) cache.quote_tick.side_effect = _quote_tick return cache def _make_iid(self, symbol_venue: str): """Return a mock InstrumentId whose str() is symbol_venue.""" iid = MagicMock() iid.__str__ = lambda self: symbol_venue return iid def setUp(self): btc_tick = self._make_quote_tick(82000.0, 82010.0, ts_event=9_999_999) eth_tick = self._make_quote_tick(1595.5, 1596.0, ts_event=8_888_888) self.cache = self._make_cache({ 'BTCUSDT.BINANCE': btc_tick, 'ETHUSDT.BINANCE': eth_tick, }) self.feed = NautilusCachePriceFeed(self.cache, venue='BINANCE') def _patch_iid(self, feed, symbol, venue): """Pre-populate the IID cache so the mock cache key matches.""" iid = MagicMock() iid.__str__ = lambda s: f'{symbol}.{venue}' feed._iid_cache[symbol] = iid def test_bid_returns_float(self): self._patch_iid(self.feed, 'BTCUSDT', 'BINANCE') self.assertAlmostEqual(self.feed.bid('BTCUSDT'), 82000.0) def test_ask_returns_float(self): self._patch_iid(self.feed, 'BTCUSDT', 'BINANCE') self.assertAlmostEqual(self.feed.ask('BTCUSDT'), 82010.0) def test_mid_is_average(self): self._patch_iid(self.feed, 'BTCUSDT', 'BINANCE') expected = (82000.0 + 82010.0) / 2.0 self.assertAlmostEqual(self.feed.mid('BTCUSDT'), expected) def test_no_quote_returns_none(self): self._patch_iid(self.feed, 'SOLUSDT', 'BINANCE') self.assertIsNone(self.feed.bid('SOLUSDT')) self.assertIsNone(self.feed.ask('SOLUSDT')) self.assertIsNone(self.feed.mid('SOLUSDT')) def test_last_update_ns_returns_ts_event(self): self._patch_iid(self.feed, 'BTCUSDT', 'BINANCE') self.assertEqual(self.feed.last_update_ns('BTCUSDT'), 9_999_999) def test_last_update_ns_no_quote_returns_zero(self): self._patch_iid(self.feed, 'SOLUSDT', 'BINANCE') self.assertEqual(self.feed.last_update_ns('SOLUSDT'), 0) def test_iid_cache_populated_on_first_call(self): """IID cache should store parsed InstrumentId after first access.""" # Pre-patch so mock cache resolves correctly self._patch_iid(self.feed, 'ETHUSDT', 'BINANCE') _ = self.feed.bid('ETHUSDT') self.assertIn('ETHUSDT', self.feed._iid_cache) def test_satisfies_price_feed_protocol(self): self.assertIsInstance(self.feed, PriceFeed) class TestPriceFeedProtocol(unittest.TestCase): """Verify all implementations satisfy the PriceFeed Protocol at runtime.""" def test_null_is_price_feed(self): self.assertIsInstance(NullPriceFeed(), PriceFeed) def test_constant_is_price_feed(self): self.assertIsInstance(ConstantPriceFeed(), PriceFeed) def test_nautilus_is_price_feed(self): feed = NautilusCachePriceFeed(MagicMock()) self.assertIsInstance(feed, PriceFeed) def test_custom_class_satisfying_protocol(self): """Any class with the right methods satisfies PriceFeed structurally.""" class MinimalFeed: def bid(self, s): return 1.0 def ask(self, s): return 1.0 def mid(self, s): return 1.0 def last_update_ns(self, s): return 0 self.assertIsInstance(MinimalFeed(), PriceFeed) def test_incomplete_class_does_not_satisfy_protocol(self): """A class missing methods should NOT satisfy PriceFeed.""" class IncompleteFeed: def bid(self, s): return 1.0 # missing ask, mid, last_update_ns self.assertNotIsInstance(IncompleteFeed(), PriceFeed) if __name__ == '__main__': unittest.main()