Skip to content

Commit ef08105

Browse files
committed
Add Polymarket fee commission tests and update py-clob-client
- Add tests for taker/maker commission calculation with non-zero fees - Verify fee handling for Polymarket 15-minute crypto prediction markets - Upgrade `py-clob-client`
1 parent a0d2795 commit ef08105

File tree

3 files changed

+201
-5
lines changed

3 files changed

+201
-5
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ dydx = [
8888
"bip-utils>=2.10.0,<3.0.0; python_version < '3.14'",
8989
"pycryptodome>=3.20.0,<4.0.0; python_version < '3.14'",
9090
]
91-
polymarket = ["py-clob-client==0.34.1,<1.0.0"]
91+
polymarket = ["py-clob-client>=0.34.4,<1.0.0"]
9292
visualization = ["plotly>=6.3.1,<7.0.0"]
9393

9494
[dependency-groups]

tests/integration_tests/adapters/polymarket/test_parsing.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from nautilus_trader.config import BacktestEngineConfig
4444
from nautilus_trader.config import LoggingConfig
4545
from nautilus_trader.model.currencies import USDC
46+
from nautilus_trader.model.currencies import USDC_POS
4647
from nautilus_trader.model.data import OrderBookDeltas
4748
from nautilus_trader.model.data import TradeTick
4849
from nautilus_trader.model.enums import AccountType
@@ -554,6 +555,201 @@ def test_parse_user_trade_to_fill_report_ts_event() -> None:
554555
assert fill_report.ts_event == 1725958681000000000 # September 10, 2024
555556

556557

558+
def test_parse_user_trade_taker_commission_with_fees() -> None:
559+
"""
560+
Test that taker commission is correctly calculated from fee_rate_bps.
561+
562+
This test uses a taker trade with 200 bps (2%) fees, as documented for Polymarket
563+
15-minute crypto prediction markets.
564+
565+
Commission = size * price * (fee_rate_bps / 10000) = 100 * 0.50 * (200 /
566+
10000) = 50 * 0.02 = 1.0 USDC
567+
568+
"""
569+
# Arrange
570+
trade_data = {
571+
"event_type": "trade",
572+
"asset_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455",
573+
"bucket_index": 0,
574+
"fee_rate_bps": "200", # 2% taker fee (Polymarket 15-min crypto markets)
575+
"id": "test-taker-trade-001",
576+
"last_update": "1725958681",
577+
"maker_address": "0x1234567890123456789012345678901234567890",
578+
"maker_orders": [
579+
{
580+
"asset_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455",
581+
"fee_rate_bps": "0",
582+
"maker_address": "0x1234567890123456789012345678901234567890",
583+
"matched_amount": "100",
584+
"order_id": "0xmaker_order_id",
585+
"outcome": "Yes",
586+
"owner": "maker-owner-id",
587+
"price": "0.50",
588+
},
589+
],
590+
"market": "0xdd22472e552920b8438158ea7238bfadfa4f736aa4cee91a6b86c39ead110917",
591+
"match_time": "1725958681",
592+
"outcome": "Yes",
593+
"owner": "taker-owner-id",
594+
"price": "0.50",
595+
"side": "BUY",
596+
"size": "100",
597+
"status": "MINED",
598+
"taker_order_id": "0xtaker_order_id",
599+
"timestamp": "1725958681000",
600+
"trade_owner": "taker-owner-id",
601+
"trader_side": "TAKER",
602+
"type": "TRADE",
603+
}
604+
605+
decoder = msgspec.json.Decoder(PolymarketUserTrade)
606+
msg = decoder.decode(msgspec.json.encode(trade_data))
607+
instrument = TestInstrumentProvider.binary_option()
608+
account_id = AccountId("POLYMARKET-001")
609+
610+
# Act
611+
fill_report = msg.parse_to_fill_report(
612+
account_id=account_id,
613+
instrument=instrument,
614+
client_order_id=None,
615+
ts_init=0,
616+
filled_user_order_id=msg.taker_order_id,
617+
)
618+
619+
# Assert
620+
# Commission = 100 * 0.50 * (200 / 10000) = 1.0 USDC.e
621+
assert fill_report.commission == Money(1.0, USDC_POS)
622+
623+
624+
def test_parse_user_trade_maker_commission_with_fees() -> None:
625+
"""
626+
Test that maker commission is correctly calculated from maker order's fee_rate_bps.
627+
628+
For maker fills, the fee_rate_bps is taken from the individual maker_order, not from
629+
the top-level trade message.
630+
631+
Commission = matched_amount * price * (fee_rate_bps / 10000) = 50 * 0.60 *
632+
(100 / 10000) = 30 * 0.01 = 0.30 USDC
633+
634+
"""
635+
# Arrange
636+
maker_owner = "maker-owner-id"
637+
maker_order_id = "0xmy_maker_order_id"
638+
trade_data = {
639+
"event_type": "trade",
640+
"asset_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455",
641+
"bucket_index": 0,
642+
"fee_rate_bps": "200", # Taker's fee (not used for maker calculation)
643+
"id": "test-maker-trade-001",
644+
"last_update": "1725958681",
645+
"maker_address": "0x1234567890123456789012345678901234567890",
646+
"maker_orders": [
647+
{
648+
"asset_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455",
649+
"fee_rate_bps": "100", # 1% maker fee
650+
"maker_address": "0x1234567890123456789012345678901234567890",
651+
"matched_amount": "50",
652+
"order_id": maker_order_id,
653+
"outcome": "Yes",
654+
"owner": maker_owner,
655+
"price": "0.60",
656+
},
657+
],
658+
"market": "0xdd22472e552920b8438158ea7238bfadfa4f736aa4cee91a6b86c39ead110917",
659+
"match_time": "1725958681",
660+
"outcome": "Yes",
661+
"owner": maker_owner,
662+
"price": "0.60",
663+
"side": "SELL",
664+
"size": "50",
665+
"status": "MINED",
666+
"taker_order_id": "0xtaker_order_id",
667+
"timestamp": "1725958681000",
668+
"trade_owner": maker_owner,
669+
"trader_side": "MAKER",
670+
"type": "TRADE",
671+
}
672+
673+
decoder = msgspec.json.Decoder(PolymarketUserTrade)
674+
msg = decoder.decode(msgspec.json.encode(trade_data))
675+
instrument = TestInstrumentProvider.binary_option()
676+
account_id = AccountId("POLYMARKET-001")
677+
678+
# Act
679+
fill_report = msg.parse_to_fill_report(
680+
account_id=account_id,
681+
instrument=instrument,
682+
client_order_id=None,
683+
ts_init=0,
684+
filled_user_order_id=maker_order_id,
685+
)
686+
687+
# Assert
688+
# Commission = 50 * 0.60 * (100 / 10000) = 0.30 USDC.e
689+
assert fill_report.commission == Money(0.30, USDC_POS)
690+
691+
692+
def test_parse_user_trade_zero_commission_with_no_fees() -> None:
693+
"""
694+
Test that commission is zero when fee_rate_bps is "0".
695+
696+
This verifies the baseline case where no fees apply (most Polymarket markets).
697+
698+
"""
699+
# Arrange
700+
trade_data = {
701+
"event_type": "trade",
702+
"asset_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455",
703+
"bucket_index": 0,
704+
"fee_rate_bps": "0",
705+
"id": "test-no-fee-trade-001",
706+
"last_update": "1725958681",
707+
"maker_address": "0x1234567890123456789012345678901234567890",
708+
"maker_orders": [
709+
{
710+
"asset_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455",
711+
"fee_rate_bps": "0",
712+
"maker_address": "0x1234567890123456789012345678901234567890",
713+
"matched_amount": "100",
714+
"order_id": "0xmaker_order_id",
715+
"outcome": "Yes",
716+
"owner": "maker-owner-id",
717+
"price": "0.50",
718+
},
719+
],
720+
"market": "0xdd22472e552920b8438158ea7238bfadfa4f736aa4cee91a6b86c39ead110917",
721+
"match_time": "1725958681",
722+
"outcome": "Yes",
723+
"owner": "taker-owner-id",
724+
"price": "0.50",
725+
"side": "BUY",
726+
"size": "100",
727+
"status": "MINED",
728+
"taker_order_id": "0xtaker_order_id",
729+
"timestamp": "1725958681000",
730+
"trade_owner": "taker-owner-id",
731+
"trader_side": "TAKER",
732+
"type": "TRADE",
733+
}
734+
735+
decoder = msgspec.json.Decoder(PolymarketUserTrade)
736+
msg = decoder.decode(msgspec.json.encode(trade_data))
737+
instrument = TestInstrumentProvider.binary_option()
738+
account_id = AccountId("POLYMARKET-001")
739+
740+
# Act
741+
fill_report = msg.parse_to_fill_report(
742+
account_id=account_id,
743+
instrument=instrument,
744+
client_order_id=None,
745+
ts_init=0,
746+
filled_user_order_id=msg.taker_order_id,
747+
)
748+
749+
# Assert
750+
assert fill_report.commission == Money(0.0, USDC_POS)
751+
752+
557753
def test_parse_empty_book_snapshot_in_backtest_engine():
558754
"""
559755
Integration test: empty book snapshots should not crash the backtest engine.

uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)