Skip to content

Commit 9a2713c

Browse files
committed
Fix Polymarket maker fill order side inversion
- Use cached order side instead of inferring from trade message - Prevents incorrect side inversion when Yes/No orders cross-match - Add regression test for maker fill side preservation - Closes #3126
1 parent 40d8cc5 commit 9a2713c

File tree

3 files changed

+56
-1
lines changed

3 files changed

+56
-1
lines changed

RELEASES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ TBD
3131
### Fixes
3232
- Fixed spawned order client_id caching in `ExecAlgorithm`, thanks for reporting @kirill-gr1
3333
- Fixed Binance instrument info dict JSON serialization, thanks for reporting @woung717
34+
- Fixed Polymarket maker fill order side inversion (#3126), thanks for reporting @santivazq
3435

3536
### Internal Improvements
3637
- Added BitMEX submit broadcaster

nautilus_trader/adapters/polymarket/execution.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1263,7 +1263,7 @@ def _handle_ws_trade_msg(self, msg: PolymarketUserTrade, wait_for_ack: bool):
12631263
venue_order_id=venue_order_id,
12641264
venue_position_id=None, # Not applicable on Polymarket
12651265
trade_id=trade_id,
1266-
order_side=msg.order_side(),
1266+
order_side=order.side,
12671267
order_type=order.order_type,
12681268
last_qty=last_qty,
12691269
last_px=last_px,

tests/integration_tests/adapters/polymarket/test_execution.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from nautilus_trader.model.currencies import USDC
3939
from nautilus_trader.model.currencies import USDC_POS
4040
from nautilus_trader.model.enums import AssetClass
41+
from nautilus_trader.model.enums import LiquiditySide
4142
from nautilus_trader.model.enums import OrderSide
4243
from nautilus_trader.model.enums import OrderType
4344
from nautilus_trader.model.enums import TimeInForce
@@ -1061,3 +1062,56 @@ def test_order_branching_logic(self):
10611062
# Assert order types are correctly identified
10621063
assert market_order.order_type == OrderType.MARKET
10631064
assert limit_order.order_type == OrderType.LIMIT
1065+
1066+
@pytest.mark.asyncio()
1067+
async def test_maker_fill_preserves_original_order_side(self, mocker):
1068+
"""
1069+
Regression test for issue #3126: Maker fill order side inversion when Yes/No
1070+
orders cross.
1071+
1072+
When a BUY order for "Yes" is matched against a BUY order for "No"
1073+
(complementary outcomes), the filled event should preserve the original order
1074+
side (BUY), not invert it based on the trade message.
1075+
1076+
"""
1077+
# Arrange - using maker order ID from user_trade1.json that matches our test wallet
1078+
instrument_id = get_polymarket_instrument_id(
1079+
"0xdd22472e552920b8438158ea7238bfadfa4f736aa4cee91a6b86c39ead110917",
1080+
"21742633143463906290569050155826241533067272736897614950488156847949938836455",
1081+
)
1082+
order = self.strategy.order_factory.limit(
1083+
instrument_id=instrument_id,
1084+
order_side=OrderSide.BUY, # Original order is BUY
1085+
quantity=Quantity.from_str("100"),
1086+
price=Price.from_str("0.518"),
1087+
)
1088+
client_order_id = order.client_order_id
1089+
venue_order_id = VenueOrderId(
1090+
"0xab679e56242324e15e59cfd488cd0f12e4fd71b153b9bfb57518898b9983145e",
1091+
)
1092+
1093+
self.cache.add_order(order, None)
1094+
self.cache.add_venue_order_id(client_order_id, venue_order_id)
1095+
filled_spy = mocker.spy(self.exec_client, "generate_order_filled")
1096+
1097+
# Act
1098+
raw_message = pkgutil.get_data(
1099+
package="tests.integration_tests.adapters.polymarket.resources.ws_messages",
1100+
resource="user_trade1.json",
1101+
)
1102+
msg = msgspec.json.decode(raw_message)
1103+
msg = self.exec_client._decoder_user_msg.decode(msgspec.json.encode(msg))
1104+
await self.exec_client._wait_for_ack_trade(msg, venue_order_id)
1105+
1106+
# Assert
1107+
filled_spy.assert_called_once()
1108+
call_kwargs = filled_spy.call_args.kwargs
1109+
1110+
# ASSERTION: order_side must match the original order (BUY), not be inverted
1111+
assert call_kwargs["order_side"] == OrderSide.BUY, (
1112+
"Maker fill should preserve original order side (BUY), not invert it. "
1113+
"This ensures correct position tracking when Yes/No orders cross-match."
1114+
)
1115+
assert call_kwargs["client_order_id"] == client_order_id
1116+
assert call_kwargs["venue_order_id"] == venue_order_id
1117+
assert call_kwargs["liquidity_side"] == LiquiditySide.MAKER

0 commit comments

Comments
 (0)