Spaces:
Running
Running
| """Canonical row model validation (PIT timestamps and visibility bounds).""" | |
| from __future__ import annotations | |
| from datetime import UTC, date, datetime, timedelta, timezone | |
| from decimal import Decimal | |
| import pytest | |
| from pydantic import ValidationError | |
| from tradebench.data.models import ( | |
| AssetMasterRow, | |
| CorporateActionKind, | |
| CorporateActionRow, | |
| DailyBarRow, | |
| FundamentalRow, | |
| ) | |
| def _day(d: date, *, hour: int = 12) -> datetime: | |
| return datetime(d.year, d.month, d.day, hour, 0, 0, tzinfo=UTC) | |
| def test_daily_bar_requires_available_at() -> None: | |
| with pytest.raises(ValidationError) as exc: | |
| DailyBarRow( | |
| asset_id="A", | |
| session_date=date(2020, 1, 2), | |
| open=Decimal("1"), | |
| high=Decimal("2"), | |
| low=Decimal("1"), | |
| close=Decimal("2"), | |
| volume=100, | |
| dollar_volume=Decimal("200"), | |
| ) | |
| assert "available_at" in str(exc.value).lower() | |
| def test_daily_bar_rejects_available_at_after_session_boundary() -> None: | |
| d = date(2020, 1, 2) | |
| bad = _day(d) + timedelta(days=1) | |
| with pytest.raises(ValidationError, match="available_at exceeds"): | |
| DailyBarRow( | |
| asset_id="A", | |
| session_date=d, | |
| open=Decimal("1"), | |
| high=Decimal("2"), | |
| low=Decimal("1"), | |
| close=Decimal("2"), | |
| volume=100, | |
| dollar_volume=Decimal("200"), | |
| available_at=bad, | |
| ) | |
| def test_daily_bar_treats_naive_available_at_as_utc() -> None: | |
| row = DailyBarRow( | |
| asset_id="A", | |
| session_date=date(2020, 1, 2), | |
| open=Decimal("1"), | |
| high=Decimal("2"), | |
| low=Decimal("1"), | |
| close=Decimal("2"), | |
| volume=100, | |
| dollar_volume=Decimal("200"), | |
| available_at=datetime(2020, 1, 2, 12, 0, 0), | |
| ) | |
| assert row.available_at == datetime(2020, 1, 2, 12, 0, 0, tzinfo=UTC) | |
| def test_daily_bar_normalizes_aware_available_at_to_utc() -> None: | |
| eastern = timezone(timedelta(hours=-5)) | |
| row = DailyBarRow( | |
| asset_id="A", | |
| session_date=date(2020, 1, 2), | |
| open=Decimal("1"), | |
| high=Decimal("2"), | |
| low=Decimal("1"), | |
| close=Decimal("2"), | |
| volume=100, | |
| dollar_volume=Decimal("200"), | |
| available_at=datetime(2020, 1, 2, 15, 0, 0, tzinfo=eastern), | |
| ) | |
| assert row.available_at == datetime(2020, 1, 2, 20, 0, 0, tzinfo=UTC) | |
| def test_fundamental_requires_available_at() -> None: | |
| with pytest.raises(ValidationError): | |
| FundamentalRow( | |
| asset_id="A", | |
| fiscal_period_end=date(2019, 12, 31), | |
| ) | |
| def test_fundamental_rejects_available_at_before_period_end() -> None: | |
| end = date(2019, 12, 31) | |
| with pytest.raises( | |
| ValidationError, | |
| match="available_at precedes fiscal_period_end", | |
| ): | |
| FundamentalRow( | |
| asset_id="A", | |
| fiscal_period_end=end, | |
| available_at=_day(date(2019, 11, 1)), | |
| ) | |
| def test_asset_master_requires_available_at() -> None: | |
| with pytest.raises(ValidationError): | |
| AssetMasterRow( | |
| asset_id="A", | |
| symbol="A", | |
| snapshot_date=date(2020, 1, 2), | |
| ) | |
| def test_asset_master_rejects_available_at_after_snapshot_boundary() -> None: | |
| snap = date(2020, 1, 2) | |
| with pytest.raises(ValidationError, match="available_at exceeds"): | |
| AssetMasterRow( | |
| asset_id="A", | |
| symbol="A", | |
| snapshot_date=snap, | |
| available_at=_day(snap) + timedelta(days=1), | |
| ) | |
| def test_corporate_action_requires_available_at() -> None: | |
| with pytest.raises(ValidationError): | |
| CorporateActionRow( | |
| asset_id="A", | |
| action_type=CorporateActionKind.CASH_DIVIDEND, | |
| effective_date=date(2020, 3, 1), | |
| ) | |
| def test_corporate_action_rejects_available_at_after_effective_boundary() -> None: | |
| eff = date(2020, 3, 1) | |
| with pytest.raises(ValidationError, match="available_at exceeds"): | |
| CorporateActionRow( | |
| asset_id="A", | |
| action_type=CorporateActionKind.SPLIT, | |
| effective_date=eff, | |
| split_from=Decimal("1"), | |
| split_to=Decimal("2"), | |
| available_at=_day(eff) + timedelta(days=2), | |
| ) | |
| def test_corporate_action_metadata_field_is_documented_as_vendor_specific() -> None: | |
| description = CorporateActionRow.model_fields["metadata"].description | |
| assert description is not None | |
| assert "vendor" in description.lower() | |