From f3d5a9273ccecf434ef7bdec6935ddb589dc76e2 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Mon, 28 Sep 2026 09:36:12 -0500 Subject: [PATCH] Reconcile perps exit-trigger price drift (v16.0.1) Re-vendor perps OpenAPI and enforce gt=0 on optional stop_loss_price/take_profit_price for the three exit-trigger request models so the client matches the upstream "strictly positive when supplied" constraint (Closes #521). --- CHANGELOG.md | 23 ++++++++++++ docs/perps.md | 4 ++- kalshi/__init__.py | 2 +- kalshi/perps/models/portfolio.py | 54 ++++++++++++++++++++++++---- pyproject.toml | 2 +- specs/perps_openapi.yaml | 12 +++---- tests/perps/test_portfolio.py | 61 +++++++++++++++++++++++++++++++- 7 files changed, 142 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f6fe70..e19a6cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/perps.md b/docs/perps.md index 8dd7af2..25a2a60 100644 --- a/docs/perps.md +++ b/docs/perps.md @@ -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 diff --git a/kalshi/__init__.py b/kalshi/__init__.py index 3246dee..afb0246 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -427,4 +427,4 @@ "Withdrawal", ] -__version__ = "16.0.0" +__version__ = "16.0.1" diff --git a/kalshi/perps/models/portfolio.py b/kalshi/perps/models/portfolio.py index 8dd9795..6813455 100644 --- a/kalshi/perps/models/portfolio.py +++ b/kalshi/perps/models/portfolio.py @@ -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) @@ -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) @@ -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"} diff --git a/pyproject.toml b/pyproject.toml index 5e5d54c..b389753 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/specs/perps_openapi.yaml b/specs/perps_openapi.yaml index 9522c58..f57ba28 100644 --- a/specs/perps_openapi.yaml +++ b/specs/perps_openapi.yaml @@ -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: >- @@ -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`. @@ -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`. diff --git a/tests/perps/test_portfolio.py b/tests/perps/test_portfolio.py index bf0365e..51f42ad 100644 --- a/tests/perps/test_portfolio.py +++ b/tests/perps/test_portfolio.py @@ -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" @@ -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")