diff --git a/additional_tests/exchanges_tests/.env.template b/additional_tests/exchanges_tests/.env.template index e3254ca01..d501511cb 100644 --- a/additional_tests/exchanges_tests/.env.template +++ b/additional_tests/exchanges_tests/.env.template @@ -16,3 +16,7 @@ PHEMEX_KEY= PHEMEX_SECRET= PHEMEX_PASSWORD= PHEMEX_SANDBOXED=true + +POLYMARKET_KEY= +POLYMARKET_SECRET= +POLYMARKET_PASSWORD= diff --git a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py index d6ba87891..47cbe7236 100644 --- a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py +++ b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py @@ -72,6 +72,7 @@ class AbstractAuthenticatedExchangeTester: ORDER_SIZE = 10 # % of portfolio to include in test orders PORTFOLIO_TYPE_FOR_SIZE = trading_constants.CONFIG_PORTFOLIO_FREE CONVERTS_ORDER_SIZE_BEFORE_PUSHING_TO_EXCHANGES = False + CONVERTS_ORDER_PRICE_BEFORE_PUSHING_TO_EXCHANGE = False ORDER_PRICE_DIFF = 20 # % of price difference compared to current price for limit and stop orders EXPECT_MISSING_ORDER_FEES_DUE_TO_ORDERS_TOO_OLD_FOR_RECENT_TRADES = False # when recent trades are limited and # closed orders fees are taken from recent trades @@ -86,6 +87,7 @@ class AbstractAuthenticatedExchangeTester: OPEN_TIMEOUT = 15 # if >0: retry fetching open/cancelled orders when created/cancelled orders are not synchronised instantly ORDER_IN_OPEN_AND_CANCELLED_ORDERS_TIMEOUT = 10 + ORDER_IMPACTS_PORTFOLIO_FREE_BALANCE = True CANCEL_TIMEOUT = 15 EDIT_TIMEOUT = 15 MIN_PORTFOLIO_SIZE = 1 @@ -616,13 +618,22 @@ async def inner_test_create_and_cancel_limit_orders(self, symbol=None, settlemen assert await self.order_in_open_orders(open_orders, limit_order, symbol=symbol) await self.check_can_get_order(limit_order) await self.sleep_before_checking_portfolio() - # assert free portfolio amount is smaller than total amount - balance = await self.get_portfolio() - locked_currency = settlement_currency if side == trading_enums.TradeOrderSide.BUY else self.ORDER_CURRENCY - assert balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_FREE] < \ - balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_TOTAL], ( - f"FALSE: {balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_FREE]} < {balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_TOTAL]}" - ) + if self.ORDER_IMPACTS_PORTFOLIO_FREE_BALANCE: + # assert free portfolio amount is smaller than total amount + balance = await self.get_portfolio() + locked_currency = settlement_currency if side == trading_enums.TradeOrderSide.BUY else self.ORDER_CURRENCY + assert balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_FREE] < \ + balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_TOTAL], ( + f"FALSE: {balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_FREE]} < {balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_TOTAL]}" + ) + else: + # assert free portfolio amount equals total amount when orders don't impact free balance + balance = await self.get_portfolio() + locked_currency = settlement_currency if side == trading_enums.TradeOrderSide.BUY else self.ORDER_CURRENCY + assert balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_FREE] == \ + balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_TOTAL], ( + f"FALSE: {balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_FREE]} == {balance[locked_currency][trading_constants.CONFIG_PORTFOLIO_TOTAL]}" + ) finally: # don't leave buy_limit as open order await self.cancel_order(limit_order) @@ -1344,7 +1355,13 @@ def _check_fetched_order_dicts(self, orders: list[dict]): def check_created_limit_order(self, order, price, size, side): self._check_order(order, size, side) - assert order.origin_price == price, f"{order.origin_price} != {price}" + if self.CONVERTS_ORDER_PRICE_BEFORE_PUSHING_TO_EXCHANGE: + # actual origin_price may vary due to price conversion + assert price * decimal.Decimal("0.8") <= order.origin_price <= price * decimal.Decimal("1.2"), ( + f"FALSE: {price * decimal.Decimal('0.8')} <= {order.origin_price} <= {price * decimal.Decimal('1.2')}" + ) + else: + assert order.origin_price == price, f"{order.origin_price} != {price}" assert isinstance(order.filled_quantity, decimal.Decimal) expected_type = personal_data.BuyLimitOrder \ if side is trading_enums.TradeOrderSide.BUY else personal_data.SellLimitOrder diff --git a/additional_tests/exchanges_tests/test_polymarket.py b/additional_tests/exchanges_tests/test_polymarket.py new file mode 100644 index 000000000..e342e9048 --- /dev/null +++ b/additional_tests/exchanges_tests/test_polymarket.py @@ -0,0 +1,130 @@ +# This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot) +# Copyright (c) 2025 Drakkar-Software, All rights reserved. +# +# OctoBot is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# OctoBot is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with OctoBot. If not, see . +import pytest + +from additional_tests.exchanges_tests import abstract_authenticated_exchange_tester + +try: + import tentacles.Trading.Exchange.polymarket.ccxt.polymarket_async +except ImportError: + pytest.skip( + reason=( + "Polymarket tentacle is not installed, skipping TestPolymarketAuthenticatedExchange" + ) + ) + +# All test coroutines will be treated as marked. +pytestmark = pytest.mark.asyncio + + +class TestPolymarketAuthenticatedExchange( + abstract_authenticated_exchange_tester.AbstractAuthenticatedExchangeTester +): + # enter exchange name as a class variable here + EXCHANGE_NAME = "polymarket" + ORDER_CURRENCY = "will-bitcoin-replace-sha-256-before-2027" + SETTLEMENT_CURRENCY = "USDC" + EXPIRATION_DATE = "261231" + SYMBOL = f"{ORDER_CURRENCY}/{SETTLEMENT_CURRENCY}:{SETTLEMENT_CURRENCY}-{EXPIRATION_DATE}" + ORDER_SIZE = 10 # % of portfolio to include in test orders + EXPECT_MISSING_FEE_IN_CANCELLED_ORDERS = False + CONVERTS_ORDER_SIZE_BEFORE_PUSHING_TO_EXCHANGES = True + CONVERTS_ORDER_PRICE_BEFORE_PUSHING_TO_EXCHANGE = True + ORDER_IMPACTS_PORTFOLIO_FREE_BALANCE = False + + async def test_get_portfolio(self): + await super().test_get_portfolio() + + async def test_get_portfolio_with_market_filter(self): + # pass if not implemented + pass + + async def test_untradable_symbols(self): + # pass if not implemented + pass + + async def test_get_max_orders_count(self): + # pass if not implemented + pass + + async def test_get_account_id(self): + # pass if not implemented + pass + + async def test_is_authenticated_request(self): + await super().test_is_authenticated_request() + + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + + async def test_get_api_key_permissions(self): + # pass if not implemented + pass + + async def test_missing_trading_api_key_permissions(self): + pass + + async def test_api_key_ip_whitelist_error(self): + # pass if not implemented + pass + + async def test_get_not_found_order(self): + await super().test_get_not_found_order() + + async def test_is_valid_account(self): + # pass if not implemented + pass + + async def test_get_special_orders(self): + # pass if not implemented + pass + + async def test_create_and_cancel_limit_orders(self): + await super().test_create_and_cancel_limit_orders() + + async def test_create_and_fill_market_orders(self): + await super().test_create_and_fill_market_orders() + + async def test_get_my_recent_trades(self): + await super().test_get_my_recent_trades() + + async def test_get_closed_orders(self): + # pass if not implemented + pass + + async def test_get_cancelled_orders(self): + # pass if not implemented + pass + + async def test_create_and_cancel_stop_orders(self): + # pass if not implemented + pass + + async def test_edit_limit_order(self): + # pass if not implemented + pass + + async def test_edit_stop_order(self): + # pass if not implemented + pass + + async def test_create_single_bundled_orders(self): + # pass if not implemented + pass + + async def test_create_double_bundled_orders(self): + # pass if not implemented + pass