feat(DATA-004): add underlying instrument selector
This commit is contained in:
164
tests/test_data_service_underlying.py
Normal file
164
tests/test_data_service_underlying.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""Tests for DATA-004: Underlying Instrument Selector routing in DataService."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.cache import CacheService
|
||||
from app.services.data_service import DataService
|
||||
|
||||
|
||||
class _CacheStub(CacheService):
|
||||
"""In-memory cache stub for unit testing."""
|
||||
|
||||
def __init__(self, initial: dict[str, object] | None = None) -> None:
|
||||
self._store = dict(initial or {})
|
||||
self.write_count = 0
|
||||
|
||||
async def get_json(self, key: str): # type: ignore[override]
|
||||
return self._store.get(key)
|
||||
|
||||
async def set_json(self, key: str, value): # type: ignore[override]
|
||||
self._store[key] = value
|
||||
self.write_count += 1
|
||||
return True
|
||||
|
||||
|
||||
class TestUnderlyingRouting:
|
||||
"""Test underlying instrument routing in DataService."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_data_service_default_underlying_is_gld(self) -> None:
|
||||
"""DataService defaults to GLD underlying."""
|
||||
cache = _CacheStub()
|
||||
service = DataService(cache)
|
||||
assert service.default_underlying == "GLD"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_data_service_default_symbol_alias(self) -> None:
|
||||
"""default_symbol is an alias for default_underlying (backward compatibility)."""
|
||||
cache = _CacheStub()
|
||||
service = DataService(cache, default_underlying="GC=F")
|
||||
assert service.default_symbol == "GC=F"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_data_service_with_custom_underlying(self) -> None:
|
||||
"""DataService can be initialized with custom underlying."""
|
||||
cache = _CacheStub()
|
||||
service = DataService(cache, default_underlying="GC=F")
|
||||
assert service.default_underlying == "GC=F"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_quote_routes_gc_f_to_futures_method(self) -> None:
|
||||
"""get_quote routes GC=F to _fetch_gc_futures."""
|
||||
cache = _CacheStub()
|
||||
service = DataService(cache)
|
||||
|
||||
# GC=F should return data with symbol GC=F
|
||||
# (source may be 'yfinance' if available, or 'fallback' if not)
|
||||
quote = await service.get_quote("GC=F")
|
||||
assert quote["symbol"] == "GC=F"
|
||||
assert quote["source"] in {"yfinance", "fallback"}
|
||||
assert "price" in quote
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_option_expirations_gc_f_returns_placeholder(self) -> None:
|
||||
"""get_option_expirations returns placeholder for GC=F."""
|
||||
cache = _CacheStub()
|
||||
service = DataService(cache)
|
||||
|
||||
# Mock a quote in cache to avoid network call
|
||||
await cache.set_json("quote:GC=F", {"symbol": "GC=F", "price": 2700.0, "source": "test"})
|
||||
|
||||
expirations = await service.get_option_expirations("GC=F")
|
||||
|
||||
assert expirations["symbol"] == "GC=F"
|
||||
assert expirations["expirations"] == []
|
||||
assert "coming soon" in expirations.get("error", "").lower()
|
||||
assert expirations["source"] == "placeholder"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_options_chain_for_expiry_gc_f_returns_placeholder(self) -> None:
|
||||
"""get_options_chain_for_expiry returns placeholder for GC=F."""
|
||||
cache = _CacheStub()
|
||||
service = DataService(cache)
|
||||
|
||||
# Mock quote and expirations in cache
|
||||
await cache.set_json("quote:GC=F", {"symbol": "GC=F", "price": 2700.0, "source": "test"})
|
||||
await cache.set_json(
|
||||
"options:GC=F:expirations", {"symbol": "GC=F", "expirations": ["2026-04-01"], "source": "test"}
|
||||
)
|
||||
|
||||
chain = await service.get_options_chain_for_expiry("GC=F", "2026-04-01")
|
||||
|
||||
assert chain["symbol"] == "GC=F"
|
||||
assert chain["calls"] == []
|
||||
assert chain["puts"] == []
|
||||
assert chain["rows"] == []
|
||||
assert "coming soon" in chain.get("error", "").lower()
|
||||
assert chain["source"] == "placeholder"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_option_expirations_gld_uses_yfinance_path(self) -> None:
|
||||
"""get_option_expirations for GLD follows normal yfinance path."""
|
||||
cache = _CacheStub()
|
||||
service = DataService(cache)
|
||||
|
||||
# Without yfinance, should fall back
|
||||
expirations = await service.get_option_expirations("GLD")
|
||||
|
||||
assert expirations["symbol"] == "GLD"
|
||||
# Should not be marked as "placeholder" (that's GC=F specific)
|
||||
assert expirations["source"] != "placeholder"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_supported_underlyings_in_domain(self) -> None:
|
||||
"""Verify supported_underlyings is accessible from domain."""
|
||||
from app.domain.instruments import Underlying, supported_underlyings
|
||||
|
||||
underlyings = supported_underlyings()
|
||||
assert len(underlyings) == 2
|
||||
assert Underlying.GLD in underlyings
|
||||
assert Underlying.GC_F in underlyings
|
||||
|
||||
|
||||
class TestPortfolioConfigUnderlying:
|
||||
"""Test PortfolioConfig underlying field."""
|
||||
|
||||
def test_portfolio_config_has_underlying_field(self) -> None:
|
||||
"""PortfolioConfig has underlying field defaulting to GLD."""
|
||||
from app.models.portfolio import PortfolioConfig
|
||||
|
||||
config = PortfolioConfig()
|
||||
assert hasattr(config, "underlying")
|
||||
assert config.underlying == "GLD"
|
||||
|
||||
def test_portfolio_config_underlying_in_to_dict(self) -> None:
|
||||
"""PortfolioConfig.to_dict includes underlying field."""
|
||||
from app.models.portfolio import PortfolioConfig
|
||||
|
||||
config = PortfolioConfig(underlying="GC=F")
|
||||
data = config.to_dict()
|
||||
|
||||
assert "underlying" in data
|
||||
assert data["underlying"] == "GC=F"
|
||||
|
||||
def test_portfolio_config_underlying_from_dict(self) -> None:
|
||||
"""PortfolioConfig.from_dict preserves underlying field."""
|
||||
from app.models.portfolio import PortfolioConfig
|
||||
|
||||
data = {
|
||||
"gold_value": 215000.0,
|
||||
"entry_price": 2150.0,
|
||||
"gold_ounces": 100.0,
|
||||
"underlying": "GC=F",
|
||||
}
|
||||
config = PortfolioConfig.from_dict(data)
|
||||
|
||||
assert config.underlying == "GC=F"
|
||||
|
||||
def test_portfolio_config_underlying_persisted_fields(self) -> None:
|
||||
"""PortfolioRepository includes underlying in persisted fields."""
|
||||
from app.models.portfolio import PortfolioRepository
|
||||
|
||||
assert "underlying" in PortfolioRepository._PERSISTED_FIELDS
|
||||
Reference in New Issue
Block a user