diff --git a/src/polymarket/__init__.py b/src/polymarket/__init__.py index 5681424..d35baef 100644 --- a/src/polymarket/__init__.py +++ b/src/polymarket/__init__.py @@ -39,6 +39,7 @@ ComboPositionMarket, ComboPositionMarketEvent, ComboPositionStatus, + ComboTradeActivity, Comment, CommentId, ConditionId, @@ -189,6 +190,7 @@ "ComboPositionMarket", "ComboPositionMarketEvent", "ComboPositionStatus", + "ComboTradeActivity", "CommentId", "ComboConditionId", "ComboMarket", diff --git a/src/polymarket/models/__init__.py b/src/polymarket/models/__init__.py index 546a4af..96f1b2d 100644 --- a/src/polymarket/models/__init__.py +++ b/src/polymarket/models/__init__.py @@ -51,6 +51,7 @@ ComboPositionMarket, ComboPositionMarketEvent, ComboPositionStatus, + ComboTradeActivity, ConversionActivity, Holder, LeaderboardCategory, @@ -166,6 +167,7 @@ "ComboPositionMarket", "ComboPositionMarketEvent", "ComboPositionStatus", + "ComboTradeActivity", "Comment", "CommentId", "ComboConditionId", diff --git a/src/polymarket/models/data/__init__.py b/src/polymarket/models/data/__init__.py index 7216845..0cf0d5a 100644 --- a/src/polymarket/models/data/__init__.py +++ b/src/polymarket/models/data/__init__.py @@ -1,6 +1,7 @@ from polymarket.models.data.activity import ( Activity, ActivityType, + ComboTradeActivity, ConversionActivity, MakerRebateActivity, MergeActivity, @@ -53,6 +54,7 @@ "ComboPositionMarket", "ComboPositionMarketEvent", "ComboPositionStatus", + "ComboTradeActivity", "ConversionActivity", "Holder", "LeaderboardCategory", diff --git a/src/polymarket/models/data/activity.py b/src/polymarket/models/data/activity.py index c954e26..1f8783b 100644 --- a/src/polymarket/models/data/activity.py +++ b/src/polymarket/models/data/activity.py @@ -14,8 +14,11 @@ parse_optional_decimal, ) from polymarket.models.types import ( + ComboConditionId, CtfConditionId, + PositionId, TokenId, + validate_combo_condition_id, validate_ctf_condition_id, validate_optional_ctf_condition_id, ) @@ -120,6 +123,7 @@ def render(self: _KnownActivityBase) -> str: class TradeActivity(_KnownActivityBase): type: Literal["TRADE"] + is_combo: Literal[False] = Field(default=False, validation_alias="isCombo") condition_id: CtfConditionId = Field(validation_alias="conditionId") token_id: TokenId = Field(validation_alias="asset") side: Literal["BUY", "SELL"] @@ -144,6 +148,29 @@ def _parse_decimal(cls, value: object) -> Decimal | None: return parse_optional_decimal(value) +class ComboTradeActivity(_KnownActivityBase): + type: Literal["TRADE"] + is_combo: Literal[True] = Field(validation_alias="isCombo") + condition_id: ComboConditionId = Field(validation_alias="conditionId") + position_id: PositionId = Field(validation_alias="asset") + side: Literal["BUY", "SELL"] + shares: Decimal = Field(validation_alias="size") + amount: Decimal + price: Decimal + title: str + icon: str | None = None + + @field_validator("condition_id", mode="before") + @classmethod + def _validate_condition_id(cls, value: object) -> ComboConditionId: + return validate_combo_condition_id(value) + + @field_validator("shares", "amount", "price", mode="before") + @classmethod + def _parse_decimal(cls, value: object) -> Decimal | None: + return parse_optional_decimal(value) + + class _MarketEventActivity(_KnownActivityBase): condition_id: CtfConditionId = Field(validation_alias="conditionId") amount: Decimal @@ -244,6 +271,7 @@ def render(self: UnknownActivity) -> str: Activity = ( TradeActivity + | ComboTradeActivity | SplitActivity | MergeActivity | RedeemActivity @@ -274,6 +302,8 @@ def parse_activity(payload: object) -> Activity: raise UnexpectedResponseError("Activity payload must be an object.") data = _normalize_activity_payload(cast(dict[str, Any], payload)) activity_type = data.get("type") + if activity_type == "TRADE" and data.get("isCombo") is True: + return ComboTradeActivity.parse_response(data) if isinstance(activity_type, str) and activity_type in _KNOWN_ACTIVITY_TYPES: cls = _KNOWN_ACTIVITY_TYPES[activity_type] return cast(Activity, cls.parse_response(data)) @@ -310,6 +340,7 @@ def _normalize_activity_payload(data: dict[str, Any]) -> dict[str, Any]: __all__ = [ "Activity", "ActivityType", + "ComboTradeActivity", "ConversionActivity", "MakerRebateActivity", "MergeActivity", diff --git a/tests/unit/test_data_activity.py b/tests/unit/test_data_activity.py index 10ba115..368e7a6 100644 --- a/tests/unit/test_data_activity.py +++ b/tests/unit/test_data_activity.py @@ -3,6 +3,7 @@ import pytest from polymarket import ( + ComboTradeActivity, ConversionActivity, MakerRebateActivity, MergeActivity, @@ -18,6 +19,8 @@ from polymarket.models.data.activity import Trade, parse_activities, parse_activity _CONDITION_ID = "0x5c19f205507ce03ff5f3be08a8090a5969ea6870cc07b902a4ca2e61dfe48fdd" +_COMBO_CONDITION_ID = "0x0365b0e193b3bcd6f3f740f5b4e9ad85b40000000000000000000000000000" +_COMBO_POSITION_ID = "1536610888192297888380575190299871560736525977576785935254302389727433588736" def _trade_payload(**overrides: object) -> dict[str, object]: @@ -92,6 +95,27 @@ def test_parse_trade_activity_uses_usdc_size_when_present() -> None: assert activity.amount == Decimal("4.20") +def test_parse_combo_trade_activity() -> None: + activity = parse_activity( + _trade_payload( + isCombo=True, + conditionId=_COMBO_CONDITION_ID, + asset=_COMBO_POSITION_ID, + outcome="", + outcomeIndex=999, + slug="", + eventSlug="", + title="Combo trade", + ) + ) + assert isinstance(activity, ComboTradeActivity) + assert activity.is_combo is True + assert activity.condition_id == _COMBO_CONDITION_ID + assert activity.position_id == _COMBO_POSITION_ID + assert activity.shares == Decimal("10") + assert activity.amount == Decimal("10") + + def test_trade_missing_required_field_raises_unexpected_response() -> None: payload = _trade_payload() del payload["price"]