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
21 changes: 8 additions & 13 deletions services/hyperliquid_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,20 +788,17 @@ async def set_position_tpsl(

# Add take profit order if specified
if tp_px is not None:
# For TP orders, use tick-aligned aggressive price
# If closing long (sell), use very low price; if closing short (buy), use very high price
slippage = 0.5 # 50% slippage for very aggressive pricing
aggressive_px = self._slippage_price(coin, not is_long, slippage)

# For TP orders, use limit order at trigger price
# This ensures the order fills at or better than the TP price
tp_order = {
"coin": coin,
"is_buy": not is_long,
"sz": float(position_size),
"limit_px": aggressive_px, # Tick-aligned aggressive price for market execution
"limit_px": float(tp_px), # Limit price = trigger price for TP
"order_type": {
"trigger": {
"triggerPx": float(tp_px),
"isMarket": True,
"isMarket": False, # Use limit order for TP
"tpsl": "tp",
}
},
Expand All @@ -812,19 +809,17 @@ async def set_position_tpsl(

# Add stop loss order if specified
if sl_px is not None:
# For SL orders, use tick-aligned aggressive price
slippage = 0.5 # 50% slippage for very aggressive pricing
aggressive_px = self._slippage_price(coin, not is_long, slippage)

# For SL orders, use market order for fast execution
# No limit_px needed when isMarket=True
sl_order = {
"coin": coin,
"is_buy": not is_long,
"sz": float(position_size),
"limit_px": aggressive_px, # Tick-aligned aggressive price for market execution
"limit_px": float(sl_px), # Use trigger price as limit_px
"order_type": {
"trigger": {
"triggerPx": float(sl_px),
"isMarket": True,
"isMarket": True, # Use market order for SL
"tpsl": "sl",
}
},
Expand Down
26 changes: 20 additions & 6 deletions tests/integration/test_oco_grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ def mock_bulk_orders(order_requests, grouping="na"):
async def test_set_position_tpsl_uses_correct_grouping(mock_service, monkeypatch):
"""测试 set_position_tpsl 使用 positionTpSl 分组"""
captured_grouping = None
captured_orders = None

def mock_bulk_orders(order_requests, grouping="na"):
nonlocal captured_grouping
nonlocal captured_grouping, captured_orders
captured_grouping = grouping
captured_orders = order_requests
return {"status": "ok", "response": {"type": "default"}}

# Mock info.user_state 返回一个仓位
Expand All @@ -74,13 +76,25 @@ def mock_user_state(address):
monkeypatch.setattr(mock_service, "_bulk_orders_with_grouping", mock_bulk_orders)
monkeypatch.setattr(mock_service.info, "user_state", mock_user_state)

# Mock _slippage_price
monkeypatch.setattr(
mock_service, "_slippage_price", lambda coin, is_buy, slippage: 45000.0
)

result = await mock_service.set_position_tpsl(coin="BTC", tp_px=47000, sl_px=43000)

assert captured_grouping == OCO_GROUP_EXISTING_POSITION
assert result["success"]
assert result["position_details"]["grouping"] == OCO_GROUP_EXISTING_POSITION

# 验证订单结构正确性
assert len(captured_orders) == 2
tp_order = next(
o for o in captured_orders if o["order_type"]["trigger"]["tpsl"] == "tp"
)
sl_order = next(
o for o in captured_orders if o["order_type"]["trigger"]["tpsl"] == "sl"
)

# 验证止盈使用限价单
assert tp_order["order_type"]["trigger"]["isMarket"] is False
assert tp_order["limit_px"] == 47000.0

# 验证止损使用市价单
assert sl_order["order_type"]["trigger"]["isMarket"] is True
assert sl_order["limit_px"] == 43000.0
Loading