Skip to content
Merged
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
12 changes: 12 additions & 0 deletions kalshi/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from datetime import datetime
from decimal import Decimal
from typing import Any, Literal

from pydantic import BaseModel
Expand Down Expand Up @@ -31,6 +32,13 @@ class Event(BaseModel):
last_updated_ts: datetime | None = None
markets: NullableList[Market] = []

# v3.18.0 backfill (#160). fee_multiplier_override is `type: number,
# format: double` per spec — Decimal (matches Market.floor_strike /
# cap_strike precedent for the same wire shape).
fee_type_override: str | None = None
fee_multiplier_override: Decimal | None = None
exchange_index: int | None = None

model_config = {"extra": "allow", "populate_by_name": True}


Expand Down Expand Up @@ -61,4 +69,8 @@ class EventMetadata(BaseModel):
market_details: list[MarketMetadata] | None = None
settlement_sources: list[SettlementSource] | None = None

# v3.18.0 backfill (#160).
competition: str | None = None
competition_scope: str | None = None

model_config = {"extra": "allow"}
6 changes: 6 additions & 0 deletions kalshi/models/historical.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from pydantic import AliasChoices, BaseModel, Field

from kalshi.models.orders import BookSideLiteral, SideLiteral
from kalshi.types import DollarDecimal, FixedPointCount

# Single-value enum per spec (MveHistoricalFilterQuery). mypy rejects plain
Expand Down Expand Up @@ -45,4 +46,9 @@ class Trade(BaseModel):
taker_side: str | None = None
created_time: datetime | None = None

# v3.18.0 backfill (#160). Mirrors Order.outcome_side / book_side from #159
# — canonical direction encoding superseding the deprecated `taker_side`.
taker_outcome_side: SideLiteral | None = None
taker_book_side: BookSideLiteral | None = None

model_config = {"extra": "allow", "populate_by_name": True}
3 changes: 3 additions & 0 deletions kalshi/models/incentive_programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class IncentiveProgram(BaseModel):
discount_factor_bps: int | None = None
target_size_fp: FixedPointCount | None = None

# v3.18.0 backfill (#160).
incentive_description: str | None = None

model_config = {"extra": "allow"}


Expand Down
4 changes: 4 additions & 0 deletions kalshi/models/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,8 @@ class Settlement(BaseModel):
validation_alias=AliasChoices("fee_cost_dollars", "fee_cost"),
)

# v3.18.0 backfill (#160). `value` is integer cents per spec
# ("Payout of a single yes contract in cents") — plain int, NOT DollarDecimal.
value: int | None = None

model_config = {"extra": "allow", "populate_by_name": True}
114 changes: 114 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@

import pytest

from kalshi.models.events import Event, EventMetadata
from kalshi.models.historical import Trade
from kalshi.models.incentive_programs import IncentiveProgram
from kalshi.models.markets import Market
from kalshi.models.orders import Fill, Order
from kalshi.models.portfolio import Settlement
from kalshi.types import to_decimal


Expand Down Expand Up @@ -353,6 +357,116 @@ def test_all_new_fields_default_to_none(self) -> None:
assert getattr(f, name) is None, f"{name} should default to None"


class TestEventV3180Fields:
"""v3.18.0 backfill (issue #160): 3 new optional fields on ``Event``."""

def test_parses_new_fields(self) -> None:
e = Event.model_validate({
"event_ticker": "EVT-001",
"fee_type_override": "quadratic",
"fee_multiplier_override": "1.25",
"exchange_index": 7,
})
assert e.fee_type_override == "quadratic"
assert e.fee_multiplier_override == Decimal("1.25")
assert isinstance(e.fee_multiplier_override, Decimal)
assert e.exchange_index == 7

def test_all_new_fields_default_to_none(self) -> None:
e = Event(event_ticker="EVT-001")
for name in ("fee_type_override", "fee_multiplier_override", "exchange_index"):
assert getattr(e, name) is None, f"{name} should default to None"


class TestEventMetadataV3180Fields:
"""v3.18.0 backfill (issue #160): 2 new optional fields on ``EventMetadata``."""

def test_parses_new_fields(self) -> None:
m = EventMetadata.model_validate({
"competition": "NBA Eastern Conference",
"competition_scope": "playoffs",
})
assert m.competition == "NBA Eastern Conference"
assert m.competition_scope == "playoffs"

def test_all_new_fields_default_to_none(self) -> None:
m = EventMetadata()
assert m.competition is None
assert m.competition_scope is None


class TestSettlementV3180Fields:
"""v3.18.0 backfill (issue #160): 1 new optional field on ``Settlement``.

``value`` is integer cents per spec ("Payout of a single yes contract in
cents") — NOT ``DollarDecimal``. Distinct from the existing
``yes_total_cost_dollars`` / ``no_total_cost_dollars`` fields.
"""

def test_value_is_int_not_decimal(self) -> None:
s = Settlement.model_validate({"ticker": "T", "value": 100})
assert s.value == 100
assert isinstance(s.value, int)
assert not isinstance(s.value, bool) # bools are ints; guard

def test_value_defaults_to_none(self) -> None:
s = Settlement(ticker="T")
assert s.value is None


class TestTradeV3180Fields:
"""v3.18.0 backfill (issue #160): 2 new optional fields on ``Trade``.

Mirrors ``Order.outcome_side`` / ``book_side`` from PR1 — canonical
direction encoding superseding the deprecated ``taker_side``.
"""

def test_parses_new_fields(self) -> None:
t = Trade.model_validate({
"trade_id": "tr-001",
"taker_outcome_side": "yes",
"taker_book_side": "bid",
})
assert t.taker_outcome_side == "yes"
assert t.taker_book_side == "bid"

def test_all_new_fields_default_to_none(self) -> None:
t = Trade(trade_id="tr-001")
assert t.taker_outcome_side is None
assert t.taker_book_side is None


class TestIncentiveProgramV3180Fields:
"""v3.18.0 backfill (issue #160): 1 new optional field on ``IncentiveProgram``."""

def test_parses_new_field(self) -> None:
p = IncentiveProgram.model_validate({
"id": "ip-001",
"market_id": "mkt-001",
"market_ticker": "MKT-001",
"incentive_type": "volume",
"start_date": "2026-01-01T00:00:00Z",
"end_date": "2026-02-01T00:00:00Z",
"period_reward": 10000,
"paid_out": False,
"incentive_description": "Liquidity provision rewards",
})
assert p.incentive_description == "Liquidity provision rewards"

def test_incentive_description_defaults_to_none(self) -> None:
p = IncentiveProgram(
id="ip-001",
market_id="mkt-001",
market_ticker="MKT-001",
incentive_type="volume",
start_date=datetime(2026, 1, 1),
end_date=datetime(2026, 2, 1),
period_reward=10000,
paid_out=False,
)
assert p.incentive_description is None


class TestErrorHierarchy:
def test_all_errors_inherit_base(self) -> None:
from kalshi.errors import (
Expand Down
Loading