|
43 | 43 | from nautilus_trader.config import BacktestEngineConfig |
44 | 44 | from nautilus_trader.config import LoggingConfig |
45 | 45 | from nautilus_trader.model.currencies import USDC |
| 46 | +from nautilus_trader.model.currencies import USDC_POS |
46 | 47 | from nautilus_trader.model.data import OrderBookDeltas |
47 | 48 | from nautilus_trader.model.data import TradeTick |
48 | 49 | from nautilus_trader.model.enums import AccountType |
@@ -554,6 +555,201 @@ def test_parse_user_trade_to_fill_report_ts_event() -> None: |
554 | 555 | assert fill_report.ts_event == 1725958681000000000 # September 10, 2024 |
555 | 556 |
|
556 | 557 |
|
| 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 | + |
557 | 753 | def test_parse_empty_book_snapshot_in_backtest_engine(): |
558 | 754 | """ |
559 | 755 | Integration test: empty book snapshots should not crash the backtest engine. |
|
0 commit comments