piker/tests/providers/test_ib_schedule.py

176 lines
5.5 KiB
Python
Raw Normal View History

Use IB schedules to reserve missing SHM history Interrupted backfill can leave stored observations separated by missing trading time. Query dated IB sessions before prepending stored history and reserve synthetic samples only during covered open sessions. Keep venue closures compressed and preserve the indexes of already published rows. Deats, - Add `Client.history_schedule()` with UTC coverage, extended hours, bounded duration, timeout, and rejection of ambiguous DST times. - Keep schedule normalization in `venues` and allocation in `.tsp._reserve`, with the project AGPL headers. - Expose schedule and synthetic-range queries through `datad.ib`. - Bound reservations by available SHM capacity and inspect only the newest 32 gaps by default via `newest_gaps`. Zero disables queries; leave unknown and oversized ranges unresolved. - Record synthetic ranges separately from OHLCV. Automatic storage writes retain provider observations; placeholders copy close. - Group the new tests under `tests/providers`. - Cover request formatting, DST, failure and capacity cases, plus real SHM/NativeDB startup placement with a fake provider. - Document a common CLI/chart segment-repair interface and defer coordinated insertion, repair execution, and reader integration. Validation: 55 targeted tests passed in the working tree; live IB schedule coverage remains unqualified. After the module moves, 33 affected tests passed again; the local review follow-up passed 18 affected cases, including four new request-budget cases. The commit runner repeats lint and isolated regression checks against this exact boundary. Prompt-IO: ai/prompt-io/codex/20260924T185456Z_1da9b760_schedule_prompt_io.md (this patch was generated in some part by `codex` using `gpt-6` (`openai`))
2026-09-24 23:09:22 +00:00
# piker: trading gear for hackers
# Copyright (C) 2018-present Tyler Goodlet (in stewardship of pikers)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
Historical schedule normalization and request-boundary regressions.
'''
import asyncio
from datetime import UTC, datetime
from types import SimpleNamespace
from zoneinfo import ZoneInfo
import pytest
from piker.brokers.ib.venues import normalize_schedule
from piker.brokers.ib.api import Client
def response(zone='America/New_York'):
'''
Describe two sessions separated by a maintenance closure.
'''
return SimpleNamespace(
timeZone=zone,
startDateTime='20260923-00:00:00',
endDateTime='20260925-00:00:00',
sessions=[SimpleNamespace(
startDateTime='20260923-18:00:00',
endDateTime='20260924-17:00:00',
)],
)
def test_schedule_uses_dated_timezone_and_coverage():
'''
Historical allocation must use dated sessions, not today's hours.
A New York September session must convert using EDT and preserve
the response's wider coverage separately from its open interval.
'''
out = normalize_schedule(response(), False)
expected = datetime(2026, 9, 23, 22, tzinfo=UTC).timestamp()
assert out['sessions'][0][0] == expected
assert out['start'] < expected < out['end']
assert out['use_rth'] is False
@pytest.mark.parametrize('stamp', [
'20261101-01:30:00', '20260308-02:30:00',
])
def test_schedule_rejects_ambiguous_or_nonexistent_time(stamp):
'''
A guessed DST fold can shift reservations by an hour.
Reject both the repeated autumn hour and nonexistent spring
hour instead of turning unknown coverage into a false closure.
'''
schedule = response()
schedule.startDateTime = stamp
with pytest.raises(ValueError, match='Ambiguous'):
normalize_schedule(schedule, False)
def test_schedule_request_uses_extended_hours_and_utc():
'''
Schedule requests must match the bars endpoint's extended hours.
Exercise the real Client method with a fake IB transport. Assert
exact UTC request bounds and day rounding, and ensure response
coverage is not replaced by the caller's requested interval.
'''
calls = []
async def find_contracts(fqme):
assert fqme == 'mnq.cme.20261218'
return ['contract']
async def schedule(contract, **kwargs):
calls.append((contract, kwargs))
return response()
client = SimpleNamespace(
find_contracts=find_contracts,
ib=SimpleNamespace(reqHistoricalScheduleAsync=schedule),
)
out = asyncio.run(Client.history_schedule(
client, 'mnq.cme.20261218',
datetime(2026, 9, 23, 0, tzinfo=UTC),
datetime(2026, 9, 24, 1, tzinfo=UTC),
))
assert calls == [('contract', {
'numDays': 2, 'endDateTime': '20260924-01:00:00',
'useRTH': False,
})]
assert out['timezone'] == 'America/New_York'
def test_schedule_timeout_is_not_an_empty_session_list():
'''
A failed schedule request must not classify the range as closed.
Block the simulated provider and apply an immediate timeout;
the adapter must propagate failure instead of returning coverage.
'''
async def find_contracts(fqme):
return ['contract']
async def schedule(*args, **kwargs):
await asyncio.sleep(100)
client = SimpleNamespace(
find_contracts=find_contracts,
ib=SimpleNamespace(reqHistoricalScheduleAsync=schedule),
)
with pytest.raises(TimeoutError):
asyncio.run(Client.history_schedule(
client, 'mnq.cme',
datetime(2026, 9, 23, tzinfo=UTC),
datetime(2026, 9, 24, tzinfo=UTC),
timeout_s=0,
))
def test_schedule_rejects_reversed_session():
'''
A malformed session must not silently become a venue closure.
Reject the response instead of dropping its reversed interval
and reserving slots using the remaining incomplete session list.
'''
schedule = response()
schedule.sessions[0].endDateTime = '20260923-17:00:00'
with pytest.raises(ValueError, match='Invalid schedule session'):
normalize_schedule(schedule, False)
def test_schedule_duration_counts_elapsed_time_across_dst():
'''
The autumn clock change makes this local day last 25 hours.
Request two elapsed days so duration rounding cannot leave an
hour uncovered by subtracting same-zone wall-clock datetimes.
'''
async def find_contracts(fqme):
return ['contract']
async def schedule(contract, **kwargs):
assert kwargs['numDays'] == 2
return response()
client = SimpleNamespace(
find_contracts=find_contracts,
ib=SimpleNamespace(reqHistoricalScheduleAsync=schedule),
)
zone = ZoneInfo('America/New_York')
asyncio.run(Client.history_schedule(
client, 'mnq.cme',
datetime(2026, 11, 1, tzinfo=zone),
datetime(2026, 11, 2, tzinfo=zone),
))