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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

All notable changes to kalshi-sdk will be documented in this file.

## 16.0.1 — 2026-09-28

Reconciles upstream perps OpenAPI description-only drift on exit-trigger
price fields (Closes #521). Re-vendored `specs/perps_openapi.yaml` (hash
`ebed03affc8ec5dd6dcd3fc4f789551f096532084a0fd639ab56d0cef0f80090`).
Core OpenAPI and AsyncAPI are unchanged. The client now rejects zero and
negative `stop_loss_price` / `take_profit_price` on the three request
models; the server already enforced this constraint. Valid positive
callers are unchanged.

### Changed (non-breaking)

- **Perps** `UpdateExitTriggerRequest`, `SetCrossExitTriggerRequest`, and
`SetIsolatedExitTriggerRequest`: `stop_loss_price` and
`take_profit_price` now use `Field(default=None, gt=0)`. Omitting the
field (`None`) remains valid; zero and negatives raise
`ValidationError` at construction. Response `ExitTrigger` is unchanged.

### Spec notes

- Perps OpenAPI re-vendored; description-only constraint that bracket
exit-trigger prices must be strictly positive when supplied.

## 16.0.0 — 2026-09-26

Reconciles upstream OpenAPI **3.30.0 → 3.31.0** plus matching perps and
Expand Down
4 changes: 3 additions & 1 deletion docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ the same subtrader). Each `FCMSubtraderRiskControls` row includes required
`member_notional_value_risk_limit` and
`effective_account_notional_value_risk_limit`.

Exit triggers (stop-loss / take-profit / trailing) sit on a position slot:
Exit triggers (stop-loss / take-profit / trailing) sit on a position slot.
Bracket `stop_loss_price` / `take_profit_price` must be strictly positive when
supplied; the client rejects zero and negative values.

```python
from decimal import Decimal
Expand Down
2 changes: 1 addition & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,4 +427,4 @@
"Withdrawal",
]

__version__ = "16.0.0"
__version__ = "16.0.1"
54 changes: 48 additions & 6 deletions kalshi/perps/models/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,22 @@ class SetCrossExitTriggerRequest(BaseModel):
anchor_order_id: str | None = None
client_trigger_id: str | None = None
kind: ExitTriggerKindLiteral | None = None
stop_loss_price: DollarDecimal | None = None
take_profit_price: DollarDecimal | None = None
stop_loss_price: DollarDecimal | None = Field(
default=None,
gt=0,
description=(
"Mark price at which the stop-loss leg fires. "
"Bracket only; must be strictly positive when supplied."
),
)
take_profit_price: DollarDecimal | None = Field(
default=None,
gt=0,
description=(
"Mark price at which the take-profit leg fires. "
"Bracket only; must be strictly positive when supplied."
),
)
trail_amount: DollarDecimal | None = None
trail_bps: int | None = Field(default=None, ge=1, le=9999)

Expand All @@ -219,8 +233,22 @@ class SetIsolatedExitTriggerRequest(BaseModel):
"""Body for PUT /margin/isolated/positions/{ticker}/exit_trigger."""

kind: ExitTriggerKindLiteral | None = None
stop_loss_price: DollarDecimal | None = None
take_profit_price: DollarDecimal | None = None
stop_loss_price: DollarDecimal | None = Field(
default=None,
gt=0,
description=(
"Mark price at which the stop-loss leg fires. "
"Bracket only; must be strictly positive when supplied."
),
)
take_profit_price: DollarDecimal | None = Field(
default=None,
gt=0,
description=(
"Mark price at which the take-profit leg fires. "
"Bracket only; must be strictly positive when supplied."
),
)
trail_amount: DollarDecimal | None = None
trail_bps: int | None = Field(default=None, ge=1, le=9999)

Expand All @@ -230,7 +258,21 @@ class SetIsolatedExitTriggerRequest(BaseModel):
class UpdateExitTriggerRequest(BaseModel):
"""Body for PUT /margin/cross/positions/{ticker}/exit_trigger/{trigger_id}."""

stop_loss_price: DollarDecimal | None = None
take_profit_price: DollarDecimal | None = None
stop_loss_price: DollarDecimal | None = Field(
default=None,
gt=0,
description=(
"Mark price at which the stop-loss leg fires. "
"Must be strictly positive when supplied."
),
)
take_profit_price: DollarDecimal | None = Field(
default=None,
gt=0,
description=(
"Mark price at which the take-profit leg fires. "
"Must be strictly positive when supplied."
),
)

model_config = {"extra": "forbid"}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "kalshi-sdk"
version = "16.0.0"
version = "16.0.1"
description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs"
readme = "README.md"
license = { text = "MIT" }
Expand Down
12 changes: 6 additions & 6 deletions specs/perps_openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2187,10 +2187,10 @@ components:
properties:
stop_loss_price:
$ref: '#/components/schemas/FixedPointDollars'
description: Mark price at which the stop-loss leg fires.
description: Mark price at which the stop-loss leg fires. Must be strictly positive when supplied.
take_profit_price:
$ref: '#/components/schemas/FixedPointDollars'
description: Mark price at which the take-profit leg fires.
description: Mark price at which the take-profit leg fires. Must be strictly positive when supplied.
SetCrossExitTriggerRequest:
type: object
description: >-
Expand Down Expand Up @@ -2231,10 +2231,10 @@ components:
description: Which trigger family this write targets. Defaults to `bracket`.
stop_loss_price:
$ref: '#/components/schemas/FixedPointDollars'
description: Mark price at which the stop-loss leg fires. Bracket only.
description: Mark price at which the stop-loss leg fires. Bracket only; must be strictly positive when supplied.
take_profit_price:
$ref: '#/components/schemas/FixedPointDollars'
description: Mark price at which the take-profit leg fires. Bracket only.
description: Mark price at which the take-profit leg fires. Bracket only; must be strictly positive when supplied.
trail_amount:
$ref: '#/components/schemas/FixedPointDollars'
description: Absolute trailing distance in dollars. Trailing only; mutually exclusive with `trail_bps`.
Expand All @@ -2259,10 +2259,10 @@ components:
description: Which trigger family this write targets. Defaults to `bracket`.
stop_loss_price:
$ref: '#/components/schemas/FixedPointDollars'
description: Mark price at which the stop-loss leg fires. Bracket only.
description: Mark price at which the stop-loss leg fires. Bracket only; must be strictly positive when supplied.
take_profit_price:
$ref: '#/components/schemas/FixedPointDollars'
description: Mark price at which the take-profit leg fires. Bracket only.
description: Mark price at which the take-profit leg fires. Bracket only; must be strictly positive when supplied.
trail_amount:
$ref: '#/components/schemas/FixedPointDollars'
description: Absolute trailing distance in dollars. Trailing only; mutually exclusive with `trail_bps`.
Expand Down
61 changes: 60 additions & 1 deletion tests/perps/test_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@
KalshiValidationError,
)
from kalshi.perps import AsyncPerpsClient, PerpsClient, PerpsConfig
from kalshi.perps.models.portfolio import MarginFill, MarginPosition, MarginTrade
from kalshi.perps.models.portfolio import (
MarginFill,
MarginPosition,
MarginTrade,
SetCrossExitTriggerRequest,
SetIsolatedExitTriggerRequest,
UpdateExitTriggerRequest,
)

BASE = "https://external-api.demo.kalshi.co/trade-api/v2"

Expand Down Expand Up @@ -604,3 +611,55 @@ async def test_async_cross_list(self, async_perps_client: AsyncPerpsClient) -> N
resp = await async_perps_client.portfolio.cross_exit_triggers("BTC-PERP")
assert resp.exit_triggers[0].kind == "bracket"
await async_perps_client.close()

@pytest.mark.parametrize(
"model",
[
SetCrossExitTriggerRequest,
SetIsolatedExitTriggerRequest,
UpdateExitTriggerRequest,
],
)
@pytest.mark.parametrize("field", ["stop_loss_price", "take_profit_price"])
@pytest.mark.parametrize("bad", [Decimal("0"), Decimal("-1")])
def test_bracket_price_rejects_zero_and_negative(
self,
model: (
type[SetCrossExitTriggerRequest]
| type[SetIsolatedExitTriggerRequest]
| type[UpdateExitTriggerRequest]
),
field: str,
bad: Decimal,
) -> None:
with pytest.raises(ValidationError):
model(**{field: bad})

@pytest.mark.parametrize(
"model",
[
SetCrossExitTriggerRequest,
SetIsolatedExitTriggerRequest,
UpdateExitTriggerRequest,
],
)
def test_bracket_price_none_and_positive_ok(
self,
model: (
type[SetCrossExitTriggerRequest]
| type[SetIsolatedExitTriggerRequest]
| type[UpdateExitTriggerRequest]
),
) -> None:
omitted = model()
assert omitted.stop_loss_price is None
assert omitted.take_profit_price is None
explicit_none = model(stop_loss_price=None, take_profit_price=None)
assert explicit_none.stop_loss_price is None
assert explicit_none.take_profit_price is None
ok = model(
stop_loss_price=Decimal("50000.0000"),
take_profit_price=Decimal("70000.0000"),
)
assert ok.stop_loss_price == Decimal("50000.0000")
assert ok.take_profit_price == Decimal("70000.0000")
Loading