2|
3|from collections.abc import Iterator
4|from datetime import date
5|from decimal import Decimal
6|import sqlite3
7|from sqlite3 import Connection
8|
9|from fastapi.testclient import TestClient
10|
11|from jarvis_finance.api.dependencies import get_db
12|from jarvis_finance.api.main import create_app
13|from jarvis_finance.api.schemas.market import QuoteRefreshRequest
14|from jarvis_finance.market_data.prices import EquityPriceQuote
15|from jarvis_finance.services.equity_service import get_equity_summary, list_equity_positions
16|from jarvis_finance.services.market_service import _quality_from_error, refresh_equity_quote, refresh_equity_quotes_batch
17|from jarvis_finance.services.portfolio_analytics import confirmed_canonical_positions, run_daily_market_valuation
18|from jarvis_finance.services.portfolio_advisor import get_portfolio_advisor_snapshot
19|from jarvis_finance.storage.migrations import apply_migrations
20|
21|NOW = "2026-07-26T12:00:00Z"
22|TARGET = date(2026, 7, 24)
23|
24|
25|def _date_text(value: date | str | None) -> str:
26|    return value.isoformat() if isinstance(value, date) else str(value or TARGET.isoformat())
27|
28|
29|def _connect() -> Connection:
30|    conn = sqlite3.connect(":memory:", check_same_thread=False)
31|    conn.row_factory = sqlite3.Row
32|    conn.execute("PRAGMA foreign_keys=ON")
33|    apply_migrations(conn)
34|    conn.execute("INSERT INTO platforms(platform_id,name,platform_type,country,default_currency,created_at) VALUES('postfinance','PostFinance','broker','CH','CHF',?)", (NOW,))
35|    conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES('etrading','postfinance','Manual Portfolio','brokerage','CHF',?)", (NOW,))
36|    return conn
37|
38|
39|def _client(conn: Connection) -> TestClient:
40|    app = create_app()
41|    def override() -> Iterator[Connection]:
42|        yield conn
43|    app.dependency_overrides[get_db] = override
44|    return TestClient(app)
45|
46|
47|def _seed_positions(conn: Connection, count: int, *, transactions: bool = False) -> None:
48|    for index in range(count):
49|        instrument_id = f"inst-{index:02d}"
50|        ticker = f"T{index:02d}"
51|        isin = f"CH{index:010d}"
59|class FreshProvider:
60|    name = "mock"
61|    def __init__(self) -> None:
62|        self.calls: list[str] = []
63|    def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
64|        self.calls.append(provider_symbol)
65|        return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=Decimal("10"), provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="fresh")
66|
67|
68|def test_batch_processes_all_22_and_resume_skips_fresh_cache(monkeypatch) -> None:
69|    conn = _connect()
70|    _seed_positions(conn, 22)
71|    provider = FreshProvider()
72|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
73|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
74|    assert result.total == result.updated == result.valued == result.coverage_total == 22
75|    assert result.complete is True
76|    assert len(provider.calls) == 22
77|    assert conn.execute("SELECT COUNT(*) FROM market_prices WHERE price_date='2026-07-24' AND close IS NOT NULL").fetchone()[0] == 22
78|
79|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: (_ for _ in ()).throw(AssertionError("fresh prices must be skipped")))
80|    resumed = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
81|    assert resumed.cached == 22
82|    assert resumed.updated == 0
83|    assert resumed.complete is True
84|
85|
86|def test_batch_does_not_treat_wrong_currency_cache_as_coverage(monkeypatch) -> None:
87|    conn = _connect()
88|    _seed_positions(conn, 1)
89|    conn.execute("""INSERT INTO market_prices(market_price_id,instrument_id,price_date,close,currency,provider,provider_symbol,provider_market,quality_status,created_at)
90|                    VALUES('wrong-cache','inst-00',?,'99','USD','fmp','T00.SW','SIX','fresh',?)""", (TARGET.isoformat(), NOW))
91|    conn.commit()
92|    provider = FreshProvider()
93|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
94|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
95|    assert result.cached == 0 and result.updated == 1 and provider.calls == ["T00.SW"]
96|
97|    wrong_exchange_conn = _connect()
98|    _seed_positions(wrong_exchange_conn, 1)
99|    wrong_exchange_conn.execute("""INSERT INTO market_prices(market_price_id,instrument_id,price_date,close,currency,provider,provider_symbol,provider_market,quality_status,created_at)
100|                    VALUES('wrong-exchange-cache','inst-00',?,'99','CHF','fmp','T00.SW','NASDAQ','fresh',?)""", (TARGET.isoformat(), NOW))
101|    wrong_exchange_conn.commit()
102|    exchange_provider = FreshProvider()
103|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: exchange_provider)
104|    exchange_result = refresh_equity_quotes_batch(wrong_exchange_conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
105|    assert exchange_result.cached == 0 and exchange_result.updated == 1 and exchange_provider.calls == ["T00.SW"]
106|
107|
108|def test_rate_limit_retries_without_marking_instrument_inactive(monkeypatch) -> None:
66|
67|
68|def test_batch_processes_all_22_and_resume_skips_fresh_cache(monkeypatch) -> None:
69|    conn = _connect()
70|    _seed_positions(conn, 22)
71|    provider = FreshProvider()
72|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
73|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
74|    assert result.total == result.updated == result.valued == result.coverage_total == 22
75|    assert result.complete is True
76|    assert len(provider.calls) == 22
77|    assert conn.execute("SELECT COUNT(*) FROM market_prices WHERE price_date='2026-07-24' AND close IS NOT NULL").fetchone()[0] == 22
78|
79|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: (_ for _ in ()).throw(AssertionError("fresh prices must be skipped")))
80|    resumed = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
81|    assert resumed.cached == 22
82|    assert resumed.updated == 0
83|    assert resumed.complete is True
84|
85|
86|def test_batch_does_not_treat_wrong_currency_cache_as_coverage(monkeypatch) -> None:
87|    conn = _connect()
88|    _seed_positions(conn, 1)
89|    conn.execute("""INSERT INTO market_prices(market_price_id,instrument_id,price_date,close,currency,provider,provider_symbol,provider_market,quality_status,created_at)
90|                    VALUES('wrong-cache','inst-00',?,'99','USD','fmp','T00.SW','SIX','fresh',?)""", (TARGET.isoformat(), NOW))
91|    conn.commit()
92|    provider = FreshProvider()
93|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
94|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
95|    assert result.cached == 0 and result.updated == 1 and provider.calls == ["T00.SW"]
96|
97|    wrong_exchange_conn = _connect()
98|    _seed_positions(wrong_exchange_conn, 1)
99|    wrong_exchange_conn.execute("""INSERT INTO market_prices(market_price_id,instrument_id,price_date,close,currency,provider,provider_symbol,provider_market,quality_status,created_at)
100|                    VALUES('wrong-exchange-cache','inst-00',?,'99','CHF','fmp','T00.SW','NASDAQ','fresh',?)""", (TARGET.isoformat(), NOW))
101|    wrong_exchange_conn.commit()
102|    exchange_provider = FreshProvider()
103|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: exchange_provider)
104|    exchange_result = refresh_equity_quotes_batch(wrong_exchange_conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
105|    assert exchange_result.cached == 0 and exchange_result.updated == 1 and exchange_provider.calls == ["T00.SW"]
106|
107|
108|def test_rate_limit_retries_without_marking_instrument_inactive(monkeypatch) -> None:
109|    conn = _connect()
110|    _seed_positions(conn, 1)
111|    class RetryProvider(FreshProvider):
112|        def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
113|            self.calls.append(provider_symbol)
114|            if len(self.calls) == 1:
115|                return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=None, provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="rate_limited", error_message="fmp_rate_limited")
80|    resumed = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
81|    assert resumed.cached == 22
82|    assert resumed.updated == 0
83|    assert resumed.complete is True
84|
85|
86|def test_batch_does_not_treat_wrong_currency_cache_as_coverage(monkeypatch) -> None:
87|    conn = _connect()
88|    _seed_positions(conn, 1)
89|    conn.execute("""INSERT INTO market_prices(market_price_id,instrument_id,price_date,close,currency,provider,provider_symbol,provider_market,quality_status,created_at)
90|                    VALUES('wrong-cache','inst-00',?,'99','USD','fmp','T00.SW','SIX','fresh',?)""", (TARGET.isoformat(), NOW))
91|    conn.commit()
92|    provider = FreshProvider()
93|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
94|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
95|    assert result.cached == 0 and result.updated == 1 and provider.calls == ["T00.SW"]
96|
97|    wrong_exchange_conn = _connect()
98|    _seed_positions(wrong_exchange_conn, 1)
99|    wrong_exchange_conn.execute("""INSERT INTO market_prices(market_price_id,instrument_id,price_date,close,currency,provider,provider_symbol,provider_market,quality_status,created_at)
100|                    VALUES('wrong-exchange-cache','inst-00',?,'99','CHF','fmp','T00.SW','NASDAQ','fresh',?)""", (TARGET.isoformat(), NOW))
101|    wrong_exchange_conn.commit()
102|    exchange_provider = FreshProvider()
103|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: exchange_provider)
104|    exchange_result = refresh_equity_quotes_batch(wrong_exchange_conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
105|    assert exchange_result.cached == 0 and exchange_result.updated == 1 and exchange_provider.calls == ["T00.SW"]
106|
107|
108|def test_rate_limit_retries_without_marking_instrument_inactive(monkeypatch) -> None:
109|    conn = _connect()
110|    _seed_positions(conn, 1)
111|    class RetryProvider(FreshProvider):
112|        def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
113|            self.calls.append(provider_symbol)
114|            if len(self.calls) == 1:
115|                return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=None, provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="rate_limited", error_message="fmp_rate_limited")
116|            return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=Decimal("10"), provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="fresh")
117|    provider = RetryProvider()
118|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
119|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=1))
120|    assert result.updated == 1 and result.results[0]["attempts"] == 2
121|    assert conn.execute("SELECT instrument_status FROM instruments WHERE instrument_id='inst-00'").fetchone()[0] != "suspected_inactive"
122|
123|
124|def test_excluded_instrument_is_not_fetched_or_valued(monkeypatch) -> None:
125|    conn = _connect()
126|    _seed_positions(conn, 1, transactions=True)
127|    conn.execute("UPDATE instruments SET valuation_policy='exclude_from_auto_price_update' WHERE instrument_id='inst-00'")
128|    conn.commit()
129|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda *_args: (_ for _ in ()).throw(AssertionError("excluded instrument contacted provider")))
90|                    VALUES('wrong-cache','inst-00',?,'99','USD','fmp','T00.SW','SIX','fresh',?)""", (TARGET.isoformat(), NOW))
91|    conn.commit()
92|    provider = FreshProvider()
93|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
94|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
95|    assert result.cached == 0 and result.updated == 1 and provider.calls == ["T00.SW"]
96|
97|    wrong_exchange_conn = _connect()
98|    _seed_positions(wrong_exchange_conn, 1)
99|    wrong_exchange_conn.execute("""INSERT INTO market_prices(market_price_id,instrument_id,price_date,close,currency,provider,provider_symbol,provider_market,quality_status,created_at)
100|                    VALUES('wrong-exchange-cache','inst-00',?,'99','CHF','fmp','T00.SW','NASDAQ','fresh',?)""", (TARGET.isoformat(), NOW))
101|    wrong_exchange_conn.commit()
102|    exchange_provider = FreshProvider()
103|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: exchange_provider)
104|    exchange_result = refresh_equity_quotes_batch(wrong_exchange_conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
105|    assert exchange_result.cached == 0 and exchange_result.updated == 1 and exchange_provider.calls == ["T00.SW"]
106|
107|
108|def test_rate_limit_retries_without_marking_instrument_inactive(monkeypatch) -> None:
109|    conn = _connect()
110|    _seed_positions(conn, 1)
111|    class RetryProvider(FreshProvider):
112|        def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
113|            self.calls.append(provider_symbol)
114|            if len(self.calls) == 1:
115|                return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=None, provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="rate_limited", error_message="fmp_rate_limited")
116|            return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=Decimal("10"), provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="fresh")
117|    provider = RetryProvider()
118|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
119|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=1))
120|    assert result.updated == 1 and result.results[0]["attempts"] == 2
121|    assert conn.execute("SELECT instrument_status FROM instruments WHERE instrument_id='inst-00'").fetchone()[0] != "suspected_inactive"
122|
123|
124|def test_excluded_instrument_is_not_fetched_or_valued(monkeypatch) -> None:
125|    conn = _connect()
126|    _seed_positions(conn, 1, transactions=True)
127|    conn.execute("UPDATE instruments SET valuation_policy='exclude_from_auto_price_update' WHERE instrument_id='inst-00'")
128|    conn.commit()
129|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda *_args: (_ for _ in ()).throw(AssertionError("excluded instrument contacted provider")))
130|    direct = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
131|    batch = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0))
132|    assert direct.close is None and direct.quality_status == "missing"
133|    assert batch.total == 0 and confirmed_canonical_positions(conn, as_of=TARGET.isoformat()) == []
134|
135|
136|def test_provider_error_classes_future_and_currency_guards(monkeypatch) -> None:
137|    assert _quality_from_error("fmp_endpoint_restricted") == "plan_restricted"
138|    assert _quality_from_error("fmp_auth_error") == "auth_error"
139|    assert _quality_from_error("fmp_rate_limited") == "rate_limited"
105|    assert exchange_result.cached == 0 and exchange_result.updated == 1 and exchange_provider.calls == ["T00.SW"]
106|
107|
108|def test_rate_limit_retries_without_marking_instrument_inactive(monkeypatch) -> None:
109|    conn = _connect()
110|    _seed_positions(conn, 1)
111|    class RetryProvider(FreshProvider):
112|        def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
113|            self.calls.append(provider_symbol)
114|            if len(self.calls) == 1:
115|                return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=None, provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="rate_limited", error_message="fmp_rate_limited")
116|            return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=Decimal("10"), provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="fresh")
117|    provider = RetryProvider()
118|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
119|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=1))
120|    assert result.updated == 1 and result.results[0]["attempts"] == 2
121|    assert conn.execute("SELECT instrument_status FROM instruments WHERE instrument_id='inst-00'").fetchone()[0] != "suspected_inactive"
122|
123|
124|def test_excluded_instrument_is_not_fetched_or_valued(monkeypatch) -> None:
125|    conn = _connect()
126|    _seed_positions(conn, 1, transactions=True)
127|    conn.execute("UPDATE instruments SET valuation_policy='exclude_from_auto_price_update' WHERE instrument_id='inst-00'")
128|    conn.commit()
129|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda *_args: (_ for _ in ()).throw(AssertionError("excluded instrument contacted provider")))
130|    direct = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
131|    batch = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0))
132|    assert direct.close is None and direct.quality_status == "missing"
133|    assert batch.total == 0 and confirmed_canonical_positions(conn, as_of=TARGET.isoformat()) == []
134|
135|
136|def test_provider_error_classes_future_and_currency_guards(monkeypatch) -> None:
137|    assert _quality_from_error("fmp_endpoint_restricted") == "plan_restricted"
138|    assert _quality_from_error("fmp_auth_error") == "auth_error"
139|    assert _quality_from_error("fmp_rate_limited") == "rate_limited"
140|    conn = _connect()
141|    _seed_positions(conn, 1)
142|
143|    future = EquityPriceQuote(provider_symbol="T00.SW", currency="CHF", close=Decimal("11"), provider="yfinance", provider_market="XSWX", price_timestamp="2026-07-25T12:00:00Z", quality_status="fresh")
144|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: type("P", (), {"get_price": lambda self, *_args, **_kwargs: future})())
145|    rejected = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
146|    assert rejected.quality_status == "future_price_rejected"
147|    assert conn.execute("SELECT COUNT(*) FROM market_prices").fetchone()[0] == 0
148|
149|    mismatch = EquityPriceQuote(provider_symbol="T00.SW", currency="USD", close=Decimal("11"), provider="yfinance", provider_market="XSWX", price_timestamp="2026-07-24T12:00:00Z", quality_status="fresh")
150|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: type("P", (), {"get_price": lambda self, *_args, **_kwargs: mismatch})())
151|    rejected = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
152|    assert rejected.quality_status == "currency_mismatch"
153|    assert conn.execute("SELECT COUNT(*) FROM market_prices").fetchone()[0] == 0
154|
117|    provider = RetryProvider()
118|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
119|    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=1))
120|    assert result.updated == 1 and result.results[0]["attempts"] == 2
121|    assert conn.execute("SELECT instrument_status FROM instruments WHERE instrument_id='inst-00'").fetchone()[0] != "suspected_inactive"
122|
123|
124|def test_excluded_instrument_is_not_fetched_or_valued(monkeypatch) -> None:
125|    conn = _connect()
126|    _seed_positions(conn, 1, transactions=True)
127|    conn.execute("UPDATE instruments SET valuation_policy='exclude_from_auto_price_update' WHERE instrument_id='inst-00'")
128|    conn.commit()
129|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda *_args: (_ for _ in ()).throw(AssertionError("excluded instrument contacted provider")))
130|    direct = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
131|    batch = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0))
132|    assert direct.close is None and direct.quality_status == "missing"
133|    assert batch.total == 0 and confirmed_canonical_positions(conn, as_of=TARGET.isoformat()) == []
134|
135|
136|def test_provider_error_classes_future_and_currency_guards(monkeypatch) -> None:
137|    assert _quality_from_error("fmp_endpoint_restricted") == "plan_restricted"
138|    assert _quality_from_error("fmp_auth_error") == "auth_error"
139|    assert _quality_from_error("fmp_rate_limited") == "rate_limited"
140|    conn = _connect()
141|    _seed_positions(conn, 1)
142|
143|    future = EquityPriceQuote(provider_symbol="T00.SW", currency="CHF", close=Decimal("11"), provider="yfinance", provider_market="XSWX", price_timestamp="2026-07-25T12:00:00Z", quality_status="fresh")
144|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: type("P", (), {"get_price": lambda self, *_args, **_kwargs: future})())
145|    rejected = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
146|    assert rejected.quality_status == "future_price_rejected"
147|    assert conn.execute("SELECT COUNT(*) FROM market_prices").fetchone()[0] == 0
148|
149|    mismatch = EquityPriceQuote(provider_symbol="T00.SW", currency="USD", close=Decimal("11"), provider="yfinance", provider_market="XSWX", price_timestamp="2026-07-24T12:00:00Z", quality_status="fresh")
150|    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: type("P", (), {"get_price": lambda self, *_args, **_kwargs: mismatch})())
151|    rejected = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
152|    assert rejected.quality_status == "currency_mismatch"
153|    assert conn.execute("SELECT COUNT(*) FROM market_prices").fetchone()[0] == 0
154|
155|
156|def test_unknown_values_remain_null_and_partial_summary_lists_all_missing() -> None:
157|    conn = _connect()
158|    empty = get_equity_summary(conn)
159|    assert empty.status == "unavailable" and empty.coverage_complete is False and empty.equity_value_chf is None
160|    _seed_positions(conn, 3, transactions=True)
161|    class PartialProvider:
162|        name = "fmp"
163|        def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
164|            close = Decimal("10") if provider_symbol == "T00.SW" else None
165|            return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=close, provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="fresh" if close else "endpoint_restricted", error_message=None if close else "fmp_endpoint_restricted")
166|    result = run_daily_market_valuation(conn, as_of=TARGET.isoformat(), price_providers={"fmp": PartialProvider()})  # type: ignore[dict-item]

__HERMES_CWD_8d46a20096ed__/home/agent/.hermes/worktrees/FinanceManager-sprint23.1__HERMES_CWD_8d46a20096ed__
