diff --git a/x10/perpetual/order_object.py b/x10/perpetual/order_object.py index 653021c..e6f4100 100644 --- a/x10/perpetual/order_object.py +++ b/x10/perpetual/order_object.py @@ -13,6 +13,7 @@ ) from x10.perpetual.orders import ( CreateOrderTpslTriggerModel, + CreateOrderConditionalTriggerModel, NewOrderModel, OrderPriceType, OrderSide, @@ -21,12 +22,17 @@ OrderType, SelfTradeProtectionLevel, TimeInForce, + OrderTriggerDirection, ) from x10.utils.date import to_epoch_millis, utc_now from x10.utils.nonce import generate_nonce from x10.utils.tpsl import calc_entire_position_size +# -------------------------------------------------- +# Trigger parameter models +# -------------------------------------------------- + @dataclass(kw_only=True) class OrderTpslTriggerParam: trigger_price: Decimal @@ -35,6 +41,18 @@ class OrderTpslTriggerParam: price_type: OrderPriceType +@dataclass(kw_only=True) +class OrderConditionalTriggerParam: + trigger_price: Decimal + trigger_price_type: OrderTriggerPriceType + direction: OrderTriggerDirection + execution_price_type: OrderPriceType + + +# -------------------------------------------------- +# Public factory +# -------------------------------------------------- + def create_order_object( *, account: StarkPerpetualAccount, @@ -43,7 +61,6 @@ def create_order_object( price: Decimal, side: OrderSide, starknet_domain: StarknetDomain, - order_type: OrderType = OrderType.LIMIT, post_only: bool = False, previous_order_external_id: Optional[str] = None, expire_time: Optional[datetime] = None, @@ -54,6 +71,12 @@ def create_order_object( builder_fee: Optional[Decimal] = None, builder_id: Optional[int] = None, reduce_only: bool = False, + + # conditional-specific + order_type: OrderType = OrderType.LIMIT, + trigger: Optional[OrderConditionalTriggerParam] = None, + + # TPSL-specific tp_sl_type: Optional[OrderTpslType] = None, take_profit: Optional[OrderTpslTriggerParam] = None, stop_loss: Optional[OrderTpslTriggerParam] = None, @@ -89,12 +112,17 @@ def create_order_object( builder_fee=builder_fee, builder_id=builder_id, reduce_only=reduce_only, + trigger=trigger, tp_sl_type=tp_sl_type, take_profit=take_profit, stop_loss=stop_loss, ) +# -------------------------------------------------- +# Internal helpers +# -------------------------------------------------- + def __create_order_tpsl_trigger_model( *, trigger_param: OrderTpslTriggerParam, @@ -135,6 +163,10 @@ def __get_opposite_side(side: OrderSide) -> OrderSide: return OrderSide.BUY if side == OrderSide.SELL else OrderSide.SELL +# -------------------------------------------------- +# Core builder +# -------------------------------------------------- + def __create_order_object( *, market: MarketModel, @@ -158,11 +190,16 @@ def __create_order_object( builder_fee: Optional[Decimal] = None, builder_id: Optional[int] = None, reduce_only: bool = False, + trigger: Optional[OrderConditionalTriggerParam] = None, tp_sl_type: Optional[OrderTpslType] = None, take_profit: Optional[OrderTpslTriggerParam] = None, stop_loss: Optional[OrderTpslTriggerParam] = None, ) -> NewOrderModel: - if order_type not in [OrderType.LIMIT, OrderType.TPSL]: + + if side not in OrderSide: + raise ValueError(f"Unexpected order side value: {side}") + + if order_type not in [OrderType.LIMIT, OrderType.TPSL, OrderType.CONDITIONAL]: raise NotImplementedError(f"{order_type} order type is not supported yet") if exact_only: @@ -178,14 +215,34 @@ def __create_order_object( if not reduce_only: raise ValueError("TPSL orders must be reduce-only") + # -------------------------------------------------- + # Conditional order validation + # -------------------------------------------------- + if order_type == OrderType.CONDITIONAL: + if trigger is None: + raise ValueError("Conditional order requires trigger") + + if tp_sl_type or take_profit or stop_loss: + raise ValueError("Conditional orders cannot include TPSL fields") + + if post_only: + raise ValueError("post_only is not supported for conditional orders") + + if price <= 0: + raise ValueError("Conditional order requires valid execution price") + + # -------------------------------------------------- + # TPSL validation + # -------------------------------------------------- + if order_type == OrderType.TPSL: if post_only: raise ValueError("TPSL orders must not be post-only") - if tp_sl_type == OrderTpslType.POSITION and synthetic_amount != Decimal(0): - raise ValueError("`amount_of_synthetic` must be 0 for entire position TPSL orders") + if tp_sl_type == OrderTpslType.POSITION: + raise NotImplementedError("`POSITION` TPSL type is not supported yet") if price != Decimal(0): - raise ValueError("`price` must be 0 for TPSL orders") + raise ValueError("`price` must be 0 for TPSL orders") if nonce is None: nonce = generate_nonce() @@ -203,8 +260,12 @@ def __create_order_object( public_key=public_key, starknet_domain=starknet_domain, ) + settlement_data = create_order_settlement_data( - side=side, synthetic_amount=synthetic_amount, price=price, ctx=settlement_data_ctx + side=side, + synthetic_amount=synthetic_amount, + price=price, + ctx=settlement_data_ctx, ) def create_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam | None): @@ -228,6 +289,7 @@ def create_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam | None): ) order_id = str(settlement_data.order_hash) if order_external_id is None else order_external_id + order = NewOrderModel( id=order_id, market=market.name, @@ -242,6 +304,16 @@ def create_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam | None): self_trade_protection_level=self_trade_protection_level, nonce=Decimal(nonce), cancel_id=previous_order_external_id, + trigger=( + CreateOrderConditionalTriggerModel( + trigger_price=trigger.trigger_price, + trigger_price_type=trigger.trigger_price_type, + direction=trigger.direction, + execution_price_type=trigger.execution_price_type, + ) + if order_type == OrderType.CONDITIONAL and trigger is not None + else None + ), settlement=settlement_data.settlement if order_type != OrderType.TPSL else None, tp_sl_type=tp_sl_type, take_profit=create_tpsl_trigger_model(take_profit),