Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
26 changes: 26 additions & 0 deletions tests/integration/test_instrument_store_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import logging
from datetime import date

import pytest
Expand Down Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down
49 changes: 36 additions & 13 deletions tt_connect/core/store/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from __future__ import annotations

import logging
from datetime import date
from typing import Any, cast

Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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."""
Expand Down
19 changes: 15 additions & 4 deletions tt_connect/core/store/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading