diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d9efa..d381515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] ### Changed +- **`list_instruments` default limit** — raised from 100 to 500. When the result count + equals the requested `limit`, a WARNING is logged (`instruments.list.truncated`) so + callers do not mistake a capped page for a full result set. Pass `limit=None` for an + unbounded scan. - Bumped `idna` (3.11 → 3.18), `python-dotenv` (1.2.1 → 1.2.2), `pytest` (9.0.2 → 9.1.1), and `pygments` (2.19.2 → 2.20.0). diff --git a/tests/integration/test_instrument_store_queries.py b/tests/integration/test_instrument_store_queries.py index 3acd67a..e8fcb9d 100644 --- a/tests/integration/test_instrument_store_queries.py +++ b/tests/integration/test_instrument_store_queries.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging from datetime import date import pytest @@ -133,6 +134,31 @@ async def test_list_instruments_rejects_invalid_filter_combinations(populated_db await mgr.queries.list_instruments(instrument_type=Commodity) +async def test_list_instruments_warns_when_result_count_equals_limit(populated_db, caplog): + mgr = _manager(populated_db) + + with caplog.at_level(logging.WARNING, logger="tt_connect.core.store.queries"): + results = await mgr.queries.list_instruments(has_derivatives=True, limit=2) + + assert len(results) == 2 + assert any("may be truncated" in r.message for r in caplog.records) + assert any( + getattr(r, "event", None) == "instruments.list.truncated" for r in caplog.records + ) + + +async def test_list_instruments_no_warning_when_under_limit_or_unbounded(populated_db, caplog): + mgr = _manager(populated_db) + + with caplog.at_level(logging.WARNING, logger="tt_connect.core.store.queries"): + under = await mgr.queries.list_instruments(has_derivatives=True, limit=10_000) + full = await mgr.queries.list_instruments(has_derivatives=True, limit=None) + + assert len(under) < 10_000 + assert len(full) == len(under) + assert not any("may be truncated" in r.message for r in caplog.records) + + # --------------------------------------------------------------------------- # get_instrument_info # --------------------------------------------------------------------------- diff --git a/tt_connect/core/store/queries.py b/tt_connect/core/store/queries.py index 2512944..02d943b 100644 --- a/tt_connect/core/store/queries.py +++ b/tt_connect/core/store/queries.py @@ -20,6 +20,7 @@ from __future__ import annotations +import logging from datetime import date from typing import Any, cast @@ -40,6 +41,11 @@ OptionChainEntry, ) +logger = logging.getLogger(__name__) + +DEFAULT_LIST_LIMIT = 500 +"""Default ``limit`` for :meth:`InstrumentQueries.list_instruments`.""" + class InstrumentQueries: """Read-only query surface for the instrument cache. @@ -77,13 +83,17 @@ async def list_instruments( strike_min: float | None = None, strike_max: float | None = None, has_derivatives: bool | None = None, - limit: int | None = 100, + limit: int | None = DEFAULT_LIST_LIMIT, ) -> list[Instrument]: """List instruments using strict canonical filters. The filter language intentionally reuses the package's existing instrument objects and enums instead of introducing a separate query object. This keeps discovery aligned with the strict public model. + + ``limit`` defaults to :data:`DEFAULT_LIST_LIMIT`. Pass ``limit=None`` + for an unbounded scan. When the result count equals ``limit``, a + WARNING is logged because the page may be truncated. """ self._validate_list_filters( instrument_type=instrument_type, @@ -98,7 +108,7 @@ async def list_instruments( ) if instrument_type in (Future,): - return cast( + results: list[Instrument] = cast( list[Instrument], await self._list_futures( exchange=exchange, @@ -107,8 +117,8 @@ async def list_instruments( limit=limit, ), ) - if instrument_type in (Option,): - return cast( + elif instrument_type in (Option,): + results = cast( list[Instrument], await self._list_options( exchange=exchange, @@ -121,15 +131,28 @@ async def list_instruments( limit=limit, ), ) - return cast( - list[Instrument], - await self._list_underlyings( - instrument_type=instrument_type, - exchange=exchange, - has_derivatives=has_derivatives, - limit=limit, - ), - ) + else: + results = cast( + list[Instrument], + await self._list_underlyings( + instrument_type=instrument_type, + exchange=exchange, + has_derivatives=has_derivatives, + limit=limit, + ), + ) + + if limit is not None and len(results) == limit: + logger.warning( + f"list_instruments returned {limit} rows (equals limit={limit}); " + "results may be truncated. Pass limit=None for a full scan.", + extra={ + "event": "instruments.list.truncated", + "limit": limit, + "count": len(results), + }, + ) + return results async def get_futures(self, underlying: Instrument) -> list[Future]: """Return all active futures for an underlying instrument, sorted by expiry.""" diff --git a/tt_connect/core/store/store.py b/tt_connect/core/store/store.py index 17846e8..7ec1c3d 100644 --- a/tt_connect/core/store/store.py +++ b/tt_connect/core/store/store.py @@ -36,6 +36,7 @@ OptionChain, ) from tt_connect.core.store.manager import InstrumentManager +from tt_connect.core.store.queries import DEFAULT_LIST_LIMIT T = TypeVar("T") @@ -84,9 +85,14 @@ async def list_instruments( strike_min: float | None = None, strike_max: float | None = None, has_derivatives: bool | None = None, - limit: int | None = 100, + limit: int | None = DEFAULT_LIST_LIMIT, ) -> list[Instrument]: - """List instruments using strict canonical filters.""" + """List instruments using strict canonical filters. + + Defaults to ``limit=500``. Pass ``limit=None`` for a full scan. When + the result count equals ``limit``, a WARNING is logged (possible + truncation). + """ return await self._queries.list_instruments( instrument_type=instrument_type, exchange=exchange, @@ -169,9 +175,14 @@ def list_instruments( strike_min: float | None = None, strike_max: float | None = None, has_derivatives: bool | None = None, - limit: int | None = 100, + limit: int | None = DEFAULT_LIST_LIMIT, ) -> list[Instrument]: - """List instruments using strict canonical filters.""" + """List instruments using strict canonical filters. + + Defaults to ``limit=500``. Pass ``limit=None`` for a full scan. When + the result count equals ``limit``, a WARNING is logged (possible + truncation). + """ return self._run( self._async.list_instruments( instrument_type=instrument_type,