diff --git a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py
index d318ad943..0f83ef8c0 100644
--- a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py
+++ b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py
@@ -16,6 +16,7 @@
import asyncio
import contextlib
import decimal
+import random
import time
import typing
import ccxt
@@ -25,10 +26,12 @@
import octobot_commons.constants as constants
import octobot_commons.enums as commons_enums
import octobot_commons.symbols as symbols
+import octobot_commons.configuration as commons_configuration
import octobot_trading.errors as trading_errors
import octobot_trading.enums as trading_enums
import octobot_trading.constants as trading_constants
import octobot_trading.exchanges as trading_exchanges
+import octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants
import octobot_trading.personal_data as personal_data
import octobot_trading.personal_data.orders as personal_data_orders
import octobot_trading.util.test_tools.exchanges_test_tools as exchanges_test_tools
@@ -51,7 +54,20 @@ class AbstractAuthenticatedExchangeTester:
ORDER_CURRENCY = "BTC"
SETTLEMENT_CURRENCY = "USDT"
SYMBOL = f"{ORDER_CURRENCY}/{SETTLEMENT_CURRENCY}"
+ TIME_FRAME = "1h"
VALID_ORDER_ID = "8bb80a81-27f7-4415-aa50-911ea46d841c"
+ SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID: dict[
+ str, (
+ str, # symbol
+ str, # order type key in 'info' dict
+ str, # order type found in 'info' dict
+ str, # parsed trading_enums.TradeOrderType
+ str, # parsed trading_enums.TradeOrderSide
+ bool, # trigger above (on higher price than order price)
+ )
+ ] = {} # stop loss / take profit and other special order types to be successfully parsed
+ # details of an order that exists but can"t be cancelled
+ UNCANCELLABLE_ORDER_ID_SYMBOL_TYPE: tuple[str, str, trading_enums.TraderOrderType] = None
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
@@ -90,12 +106,17 @@ class AbstractAuthenticatedExchangeTester:
CHECK_EMPTY_ACCOUNT = False # set True when the account to check has no funds. Warning: does not check order
# parse/create/fill/cancel or portfolio & trades parsing
IS_BROKER_ENABLED_ACCOUNT = True # set False when this test account can't generate broker fees
+ # set True when this exchange used to have symbols that can't be traded through API (ex: MEXC)
+ USED_TO_HAVE_UNTRADABLE_SYMBOL = False
# Implement all "test_[name]" methods, call super() to run the test, pass to ignore it.
# Override the "inner_test_[name]" method to override a test content.
# Add method call to subclasses to be able to run them independently
async def test_get_portfolio(self):
+ # encoded_a = _get_encoded_value("") # tool to get encoded values
+ # encoded_b = _get_encoded_value("")
+ # encoded_c = _get_encoded_value("")
async with self.local_exchange_manager():
await self.inner_test_get_portfolio()
@@ -141,6 +162,98 @@ def check_portfolio_content(self, portfolio):
at_least_one_value = True
assert at_least_one_value
+ async def test_untradable_symbols(self):
+ await self.inner_test_untradable_symbols()
+
+ async def inner_test_untradable_symbols(self):
+ if not self.USED_TO_HAVE_UNTRADABLE_SYMBOL:
+ # nothing to do
+ return
+ async with self.local_exchange_manager():
+ all_symbols = self.exchange_manager.exchange.get_all_available_symbols()
+ tradable_symbols = await self.exchange_manager.exchange.get_all_tradable_symbols()
+ assert len(all_symbols) > len(tradable_symbols)
+ untradable_symbols = [
+ symbol
+ for symbol in all_symbols
+ if symbol not in tradable_symbols
+ and symbol.endswith(f"/{self.SETTLEMENT_CURRENCY}")
+ and (
+ symbols.parse_symbol(symbol).is_spot()
+ if self.EXCHANGE_TYPE == trading_enums.ExchangeTypes.SPOT.value
+ else symbols.parse_symbol(symbol).is_future()
+ )
+ ]
+ tradable_symbols = [
+ symbol
+ for symbol in all_symbols
+ if symbol in tradable_symbols
+ and symbol.endswith(f"/{self.SETTLEMENT_CURRENCY}")
+ and (
+ symbols.parse_symbol(symbol).is_spot()
+ if self.EXCHANGE_TYPE == trading_enums.ExchangeTypes.SPOT.value
+ else symbols.parse_symbol(symbol).is_future()
+ )
+ ]
+ # has untradable symbols of this trading type
+ assert len(untradable_symbols) > 0
+ first_untradable_symbol = untradable_symbols[0]
+ # Public data
+ # market status is available
+ assert ccxt_constants.CCXT_INFO in self.exchange_manager.exchange.get_market_status(first_untradable_symbol)
+ # fetching ohlcv is ok
+ assert len(
+ await self.exchange_manager.exchange.get_symbol_prices(
+ first_untradable_symbol, commons_enums.TimeFrames(self.TIME_FRAME)
+ )
+ ) > 5
+ # fetching kline is ok
+ kline = await self.exchange_manager.exchange.get_kline_price(
+ first_untradable_symbol, commons_enums.TimeFrames(self.TIME_FRAME)
+ )
+ assert len(kline) == 1
+ assert len(kline[0]) == 6
+ # fetching ticker is ok
+ ticker = await self.exchange_manager.exchange.get_price_ticker(first_untradable_symbol)
+ assert ticker
+ price = ticker[trading_enums.ExchangeConstantsTickersColumns.CLOSE.value]
+ assert price > 0
+ # fetching recent trades is ok
+ recent_trades = await self.exchange_manager.exchange.get_recent_trades(first_untradable_symbol)
+ assert len(recent_trades) > 1
+ # fetching order book is ok
+ order_book = await self.exchange_manager.exchange.get_order_book(first_untradable_symbol)
+ assert len(order_book[trading_enums.ExchangeConstantsOrderBookInfoColumns.ASKS.value]) > 0
+ # is in all tickers
+ all_tickers = await self.exchange_manager.exchange.get_all_currencies_price_ticker()
+ assert all_tickers[first_untradable_symbol][trading_enums.ExchangeConstantsTickersColumns.CLOSE.value] > 0
+ # Orders
+ # try creating & cancelling orders on 5 random tradable and untradable symbols
+ symbols_to_test = 5
+ tradable_stepper = random.randint(1, len(tradable_symbols) - 2)
+ tradable_indexes = [tradable_stepper * i for i in range(0, symbols_to_test)]
+ untradable_stepper = random.randint(1, len(untradable_symbols) - 2)
+ untradable_indexes = [untradable_stepper * i for i in range(0, symbols_to_test)]
+ to_test_symbols = [
+ tradable_symbols[i % (len(tradable_symbols) - 1)] for i in tradable_indexes
+ ] + [
+ untradable_symbols[i % (len(untradable_symbols) - 1)] for i in untradable_indexes
+ ]
+ for i, symbol in enumerate(to_test_symbols):
+ ticker = await self.exchange_manager.exchange.get_price_ticker(symbol)
+ price = ticker[trading_enums.ExchangeConstantsTickersColumns.CLOSE.value]
+ price = self.get_order_price(decimal.Decimal(str(price)), False, symbol=symbol)
+ size = self.get_order_size(
+ await self.get_portfolio(), price, symbol=symbol,
+ settlement_currency=self.SETTLEMENT_CURRENCY
+ )
+ buy_limit = await self.create_limit_order(
+ price, size, trading_enums.TradeOrderSide.BUY,
+ symbol=symbol
+ )
+ await self.cancel_order(buy_limit)
+ print(f"{i+1}/{len(to_test_symbols)} : {symbol} Create & cancel order OK")
+
async def test_get_account_id(self):
async with self.local_exchange_manager():
await self.inner_test_get_account_id()
@@ -278,10 +391,7 @@ async def inner_test_get_api_key_permissions(self):
"_get_api_key_rights_using_order", mock.AsyncMock(side_effect=origin_get_api_key_rights_using_order)
) as _get_api_key_rights_using_order_mock:
permissions = await self.exchange_manager.exchange_backend._get_api_key_rights()
- assert len(permissions) > 0
- assert trading_backend.enums.APIKeyRights.READING in permissions
- assert trading_backend.enums.APIKeyRights.SPOT_TRADING in permissions
- assert trading_backend.enums.APIKeyRights.FUTURES_TRADING in permissions
+ self._ensure_required_permissions(permissions)
if self.USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS:
# failed ? did not use _get_api_key_rights_using_order while expected
_get_api_key_rights_using_order_mock.assert_called_once()
@@ -289,6 +399,11 @@ async def inner_test_get_api_key_permissions(self):
# failed ? used _get_api_key_rights_using_order when not expected
_get_api_key_rights_using_order_mock.assert_not_called()
+ def _ensure_required_permissions(self, permissions):
+ assert len(permissions) > 0
+ assert trading_backend.enums.APIKeyRights.READING in permissions
+ assert trading_backend.enums.APIKeyRights.SPOT_TRADING in permissions
+
async def test_missing_trading_api_key_permissions(self):
async with self.local_exchange_manager(identifiers_suffix="_READONLY"):
await self.inner_test_missing_trading_api_key_permissions()
@@ -327,8 +442,53 @@ async def inner_test_is_valid_account(self):
assert isinstance(error, str)
assert len(error) > 0
+
+ async def test_get_special_orders(self):
+ if self.SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID:
+ async with self.local_exchange_manager():
+ await self.inner_test_get_special_orders()
+
+ async def inner_test_get_special_orders(self):
+ # open_orders = await self.get_open_orders(_symbols=["TAO/USDT"])
+ # print(open_orders) # to print special orders when they are open
+ # return
+ for exchange_id, order_details in self.SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID.items():
+ symbol, info_key, info_type, expected_type, expected_side, expected_trigger_above = order_details
+ fetched_order = await self.exchange_manager.exchange.get_order(exchange_id, symbol=symbol)
+ assert fetched_order is not None
+ self._check_fetched_order_dicts([fetched_order])
+ # ensure parsing order doesn't crash
+ parsed = personal_data.create_order_instance_from_raw(self.exchange_manager.trader, fetched_order)
+ assert isinstance(parsed, personal_data.Order)
+
+ assert fetched_order[trading_enums.ExchangeConstantsOrderColumns.SYMBOL.value] == symbol
+ found_type = fetched_order[ccxt_constants.CCXT_INFO][info_key]
+ assert found_type == info_type, f"[{exchange_id}]: {found_type} != {info_type}"
+ parsed_type = fetched_order[trading_enums.ExchangeConstantsOrderColumns.TYPE.value]
+ assert parsed_type == expected_type, f"[{exchange_id}]: {parsed_type} != {expected_type}"
+ assert fetched_order[trading_enums.ExchangeConstantsOrderColumns.SIDE.value] == expected_side
+ if expected_trigger_above is None:
+ assert trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value not in fetched_order
+ else:
+ parsed_trigger_above = fetched_order[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value]
+ assert parsed_trigger_above == expected_trigger_above, (
+ f"[{exchange_id}]: {parsed_trigger_above} != {expected_trigger_above}"
+ )
+ if isinstance(parsed, personal_data.LimitOrder):
+ assert parsed.trigger_above == parsed_trigger_above
+ if expected_type == trading_enums.TradeOrderType.LIMIT.value:
+ assert fetched_order[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] > 0
+ elif expected_type == trading_enums.TradeOrderType.STOP_LOSS.value:
+ assert fetched_order[trading_enums.ExchangeConstantsOrderColumns.STOP_PRICE.value] > 0
+ elif expected_type == trading_enums.TradeOrderType.UNSUPPORTED.value:
+ assert isinstance(parsed, personal_data.UnsupportedOrder)
+ else:
+ # ensure all cases are covered, otherwise there is a problem in order type parsing
+ assert expected_type == trading_enums.TradeOrderType.MARKET
+
async def test_create_and_cancel_limit_orders(self):
async with self.local_exchange_manager():
+ await self.inner_test_cancel_uncancellable_order()
await self.inner_test_create_and_cancel_limit_orders()
async def inner_test_create_and_cancel_limit_orders(self, symbol=None, settlement_currency=None, margin_type=None):
@@ -396,6 +556,12 @@ async def inner_test_create_and_cancel_limit_orders(self, symbol=None, settlemen
assert await self.order_not_in_open_orders(open_orders, buy_limit, symbol=symbol)
assert await self.order_in_cancelled_orders(cancelled_orders, buy_limit, symbol=symbol)
+ async def inner_test_cancel_uncancellable_order(self):
+ if self.UNCANCELLABLE_ORDER_ID_SYMBOL_TYPE:
+ order_id, symbol, order_type = self.UNCANCELLABLE_ORDER_ID_SYMBOL_TYPE
+ with pytest.raises(trading_errors.ExchangeOrderCancelError):
+ await self.exchange_manager.exchange.cancel_order(order_id, symbol, order_type)
+
async def test_create_and_fill_market_orders(self):
async with self.local_exchange_manager():
await self.inner_test_create_and_fill_market_orders()
@@ -747,12 +913,12 @@ async def get_my_recent_trades(self, exchange_data=None):
async def get_closed_orders(self, symbol=None):
return await self.exchange_manager.exchange.get_closed_orders(symbol or self.SYMBOL)
- async def get_cancelled_orders(self, exchange_data=None, force_fetch=False):
+ async def get_cancelled_orders(self, exchange_data=None, force_fetch=False, _symbols=None):
if not force_fetch and not self.exchange_manager.exchange.SUPPORT_FETCHING_CANCELLED_ORDERS:
# skipped
return []
exchange_data = exchange_data or self.get_exchange_data()
- return await exchanges_test_tools.get_cancelled_orders(self.exchange_manager, exchange_data)
+ return await exchanges_test_tools.get_cancelled_orders(self.exchange_manager, exchange_data, symbols=_symbols)
async def check_require_closed_orders_from_recent_trades(self, symbol=None):
if self.exchange_manager.exchange.REQUIRE_CLOSED_ORDERS_FROM_RECENT_TRADES:
@@ -1088,9 +1254,9 @@ def get_order_price(self, price, is_above_price, symbol=None, price_diff=None):
price * (decimal.Decimal(str(multiplier)))
)
- async def get_open_orders(self, exchange_data=None):
+ async def get_open_orders(self, exchange_data=None, _symbols=None):
exchange_data = exchange_data or self.get_exchange_data()
- orders = await exchanges_test_tools.get_open_orders(self.exchange_manager, exchange_data)
+ orders = await exchanges_test_tools.get_open_orders(self.exchange_manager, exchange_data, symbols=_symbols)
self.check_duplicate(orders)
self._check_fetched_order_dicts(orders)
return orders
@@ -1362,7 +1528,7 @@ def get_exchange_data(self, symbol=None, all_symbols=None) -> exchange_data_impo
exchange_details={"name": self.exchange_manager.exchange_name},
markets=[
{
- "id": s, "symbol": s, "info": {}, "time_frame": "1h",
+ "id": s, "symbol": s, "info": {}, "time_frame": self.TIME_FRAME,
"close": [0], "open": [0], "high": [0], "low": [0], "volume": [0], "time": [0] # todo
}
for s in _symbols
@@ -1377,3 +1543,7 @@ def market_filter(market):
)
return market_filter
+
+
+def _get_encoded_value(raw) -> str:
+ return commons_configuration.encrypt(raw).decode()
diff --git a/additional_tests/exchanges_tests/abstract_authenticated_future_exchange_tester.py b/additional_tests/exchanges_tests/abstract_authenticated_future_exchange_tester.py
index d6ab6805d..1e23387e3 100644
--- a/additional_tests/exchanges_tests/abstract_authenticated_future_exchange_tester.py
+++ b/additional_tests/exchanges_tests/abstract_authenticated_future_exchange_tester.py
@@ -15,12 +15,12 @@
# License along with OctoBot. If not, see .
import contextlib
import decimal
-import ccxt
import pytest
import octobot_trading.enums as trading_enums
import octobot_trading.constants as trading_constants
import octobot_trading.errors as trading_errors
+import trading_backend.enums
from additional_tests.exchanges_tests import abstract_authenticated_exchange_tester
@@ -33,7 +33,6 @@ class AbstractAuthenticatedFutureExchangeTester(
INVERSE_SYMBOL = None
MIN_PORTFOLIO_SIZE = 2 # ensure fetching currency for linear and inverse
SUPPORTS_GET_LEVERAGE = True
- SUPPORTS_SET_LEVERAGE = True
SUPPORTS_EMPTY_POSITION_SET_MARGIN_TYPE = True
async def test_get_empty_linear_and_inverse_positions(self):
@@ -85,9 +84,12 @@ async def inner_test_get_and_set_leverage(self):
assert origin_leverage != trading_constants.ZERO
if self.SUPPORTS_GET_LEVERAGE:
assert origin_leverage == await self.get_leverage()
- if not self.SUPPORTS_SET_LEVERAGE:
- return
new_leverage = origin_leverage + 1
+ if not self.exchange_manager.exchange.UPDATE_LEVERAGE_FROM_API:
+ # can't set from api: make sure of that
+ with pytest.raises(trading_errors.NotSupported):
+ await self.exchange_manager.exchange.connector.set_symbol_leverage(self.SYMBOL, float(new_leverage))
+ return
await self.set_leverage(new_leverage)
await self._check_margin_type_and_leverage(origin_margin_type, new_leverage) # did not change margin type
# change leverage back to origin value
@@ -190,6 +192,10 @@ async def _inner_test_create_and_cancel_limit_orders_for_margin_type(
symbol=self.INVERSE_SYMBOL, settlement_currency=self.ORDER_CURRENCY, margin_type=margin_type
)
+ def _ensure_required_permissions(self, permissions):
+ super()._ensure_required_permissions(permissions)
+ assert trading_backend.enums.APIKeyRights.FUTURES_TRADING in permissions
+
async def inner_test_create_and_fill_market_orders(self):
portfolio = await self.get_portfolio()
position = await self.get_position()
diff --git a/additional_tests/exchanges_tests/test_ascendex.py b/additional_tests/exchanges_tests/test_ascendex.py
index f43b2800d..845e55cdc 100644
--- a/additional_tests/exchanges_tests/test_ascendex.py
+++ b/additional_tests/exchanges_tests/test_ascendex.py
@@ -40,6 +40,9 @@ async def test_get_portfolio_with_market_filter(self):
# pass if not implemented
pass
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -63,6 +66,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_binance.py b/additional_tests/exchanges_tests/test_binance.py
index 7755dcc6a..f93ea0a30 100644
--- a/additional_tests/exchanges_tests/test_binance.py
+++ b/additional_tests/exchanges_tests/test_binance.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public
# License along with OctoBot. If not, see .
import pytest
+import octobot_trading.enums
from additional_tests.exchanges_tests import abstract_authenticated_exchange_tester
@@ -41,6 +42,38 @@ class TestBinanceAuthenticatedExchange(
IS_BROKER_ENABLED_ACCOUNT = False
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented
+ SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID: dict[
+ str, (
+ str, # symbol
+ str, # order type key in 'info' dict
+ str, # order type found in 'info' dict
+ str, # parsed trading_enums.TradeOrderType
+ str, # parsed trading_enums.TradeOrderSide
+ bool, # trigger above (on higher price than order price)
+ )
+ ] = {
+ "6799804660": (
+ "BNB/USDT", "type", "TAKE_PROFIT",
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.SELL.value, True
+ ),
+ '6799810041': (
+ "BNB/USDT", "type", "STOP_LOSS",
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.SELL.value, False
+ ),
+ '6799798838': (
+ "BNB/USDT", "type", "TAKE_PROFIT_LIMIT",
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.SELL.value, True
+ ),
+ '6799795001': (
+ "BNB/USDT", "type", "STOP_LOSS_LIMIT",
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.SELL.value, False
+ ),
+ } # stop loss / take profit and other special order types to be successfully parsed
+ # details of an order that exists but can"t be cancelled
+ UNCANCELLABLE_ORDER_ID_SYMBOL_TYPE: tuple[str, str, octobot_trading.enums.TraderOrderType] = (
+ "6799798838", "BNB/USDT", octobot_trading.enums.TraderOrderType.BUY_LIMIT.value
+ )
+
async def test_get_portfolio(self):
await super().test_get_portfolio()
@@ -48,6 +81,9 @@ async def test_get_portfolio(self):
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
await super().test_get_account_id()
@@ -69,6 +105,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_binance_futures.py b/additional_tests/exchanges_tests/test_binance_futures.py
index 57cc09cde..f3ff05e8c 100644
--- a/additional_tests/exchanges_tests/test_binance_futures.py
+++ b/additional_tests/exchanges_tests/test_binance_futures.py
@@ -15,6 +15,7 @@
# License along with OctoBot. If not, see .
import pytest
+import octobot_trading.enums
from additional_tests.exchanges_tests import abstract_authenticated_future_exchange_tester, \
abstract_authenticated_exchange_tester
@@ -38,6 +39,38 @@ class TestBinanceFuturesAuthenticatedExchange(
EXPECTED_QUOTE_MIN_ORDER_SIZE = 200 # min quote value of orders to create (used to check market status parsing)
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented
+ SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID: dict[
+ str, (
+ str, # symbol
+ str, # order type key in 'info' dict
+ str, # order type found in 'info' dict
+ str, # parsed trading_enums.TradeOrderType
+ str, # parsed trading_enums.TradeOrderSide
+ bool, # trigger above (on higher price than order price)
+ )
+ ] = {
+ "4075521283": (
+ "BTC/USDT:USDT", "type", "TAKE_PROFIT_MARKET",
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.SELL.value, True
+ ),
+ '622529': (
+ "BTC/USDC:USDC", "type", "STOP_MARKET",
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.SELL.value, False
+ ),
+ '4076521927': (
+ "BTC/USDT:USDT", "type", "TAKE_PROFIT",
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.BUY.value, False
+ ),
+ '4076521976': (
+ "BTC/USDT:USDT", "type", "STOP",
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.SELL.value, False
+ ),
+ } # stop loss / take profit and other special order types to be successfully parsed
+ # details of an order that exists but can"t be cancelled
+ UNCANCELLABLE_ORDER_ID_SYMBOL_TYPE: tuple[str, str, octobot_trading.enums.TraderOrderType] = (
+ "4076521927", "BTC/USDT:USDT", octobot_trading.enums.TraderOrderType.BUY_LIMIT.value
+ )
+
async def _set_account_types(self, account_types):
# todo remove this and use both types when exchange-side multi portfolio is enabled
self.exchange_manager.exchange._futures_account_types = account_types
@@ -48,6 +81,9 @@ async def test_get_portfolio(self):
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter() # can have small variations failing the test when positions are open
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
await super().test_get_account_id()
@@ -86,6 +122,9 @@ async def test_get_and_set_leverage(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_bingx.py b/additional_tests/exchanges_tests/test_bingx.py
index 8d402a4d1..dce470c2e 100644
--- a/additional_tests/exchanges_tests/test_bingx.py
+++ b/additional_tests/exchanges_tests/test_bingx.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public
# License along with OctoBot. If not, see .
import pytest
+import octobot_trading.enums
from additional_tests.exchanges_tests import abstract_authenticated_exchange_tester
@@ -38,12 +39,47 @@ class TestBingxAuthenticatedExchange(
VALID_ORDER_ID = "1812980957928929280"
+ SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID: dict[
+ str, (
+ str, # symbol
+ str, # order type key in 'info' dict
+ str, # order type found in 'info' dict
+ str, # parsed trading_enums.TradeOrderType
+ str, # parsed trading_enums.TradeOrderSide
+ bool, # trigger above (on higher price than order price)
+ )
+ ] = {
+ "1877004154170146816": (
+ "TAO/USDT", "type", "TAKE_STOP_MARKET",
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.SELL.value, False
+ ),
+ '1877004191864356864': (
+ "TAO/USDT", "type", "TAKE_STOP_MARKET",
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.SELL.value, True
+ ),
+ '1877004220704391168': (
+ "TAO/USDT", "type", "TAKE_STOP_LIMIT",
+ octobot_trading.enums.TradeOrderType.UNSUPPORTED.value, octobot_trading.enums.TradeOrderSide.SELL.value, None
+ ),
+ '1877004292053696512': (
+ "TAO/USDT", "type", "TAKE_STOP_LIMIT",
+ octobot_trading.enums.TradeOrderType.UNSUPPORTED.value, octobot_trading.enums.TradeOrderSide.SELL.value, None
+ ),
+ } # stop loss / take profit and other special order types to be successfully parsed
+ # details of an order that exists but can"t be cancelled
+ UNCANCELLABLE_ORDER_ID_SYMBOL_TYPE: tuple[str, str, octobot_trading.enums.TraderOrderType] = (
+ "1877004292053696512", "TAO/USDT", octobot_trading.enums.TraderOrderType.SELL_LIMIT.value
+ )
+
async def test_get_portfolio(self):
await super().test_get_portfolio()
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
await super().test_get_account_id()
@@ -65,6 +101,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_bitget.py b/additional_tests/exchanges_tests/test_bitget.py
index 33c03e91d..63f46c6c3 100644
--- a/additional_tests/exchanges_tests/test_bitget.py
+++ b/additional_tests/exchanges_tests/test_bitget.py
@@ -42,6 +42,9 @@ async def test_get_portfolio_with_market_filter(self):
# pass if not implemented
pass
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -65,6 +68,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_bitmart.py b/additional_tests/exchanges_tests/test_bitmart.py
index f1eacab34..7c6700ac2 100644
--- a/additional_tests/exchanges_tests/test_bitmart.py
+++ b/additional_tests/exchanges_tests/test_bitmart.py
@@ -40,6 +40,9 @@ async def test_get_portfolio(self):
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -63,6 +66,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_bybit.py b/additional_tests/exchanges_tests/test_bybit.py
index 1eb4dde3c..b893070f4 100644
--- a/additional_tests/exchanges_tests/test_bybit.py
+++ b/additional_tests/exchanges_tests/test_bybit.py
@@ -44,6 +44,9 @@ async def test_get_portfolio_with_market_filter(self):
# pass if not implemented
pass
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -67,6 +70,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_bybit_futures.py b/additional_tests/exchanges_tests/test_bybit_futures.py
index 950b07c0b..a90efa93a 100644
--- a/additional_tests/exchanges_tests/test_bybit_futures.py
+++ b/additional_tests/exchanges_tests/test_bybit_futures.py
@@ -47,6 +47,9 @@ async def test_get_portfolio_with_market_filter(self):
# pass if not implemented
pass
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -79,6 +82,9 @@ async def test_get_and_set_leverage(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_coinbase.py b/additional_tests/exchanges_tests/test_coinbase.py
index 7b3790f06..6e484f29a 100644
--- a/additional_tests/exchanges_tests/test_coinbase.py
+++ b/additional_tests/exchanges_tests/test_coinbase.py
@@ -15,6 +15,7 @@
# License along with OctoBot. If not, see .
import pytest
+import octobot_trading.enums
from additional_tests.exchanges_tests import abstract_authenticated_exchange_tester
# All test coroutines will be treated as marked.
@@ -29,7 +30,7 @@ class TestCoinbaseAuthenticatedExchange(
ORDER_CURRENCY = "ADA"
SETTLEMENT_CURRENCY = "BTC"
SYMBOL = f"{ORDER_CURRENCY}/{SETTLEMENT_CURRENCY}"
- ORDER_SIZE = 5 # % of portfolio to include in test orders
+ ORDER_SIZE = 25 # % of portfolio to include in test orders
CONVERTS_ORDER_SIZE_BEFORE_PUSHING_TO_EXCHANGES = True
VALID_ORDER_ID = "8bb80a81-27f7-4415-aa50-911ea46d841c"
USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS = True # set True when api key rights can't be checked using a
@@ -37,12 +38,43 @@ class TestCoinbaseAuthenticatedExchange(
IS_BROKER_ENABLED_ACCOUNT = False
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented
+ SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID: dict[
+ str, (
+ str, # symbol
+ str, # order type key in 'info' dict
+ str, # order type found in 'info' dict
+ str, # parsed trading_enums.TradeOrderType
+ str, # parsed trading_enums.TradeOrderSide
+ bool, # trigger above (on higher price than order price)
+ )
+ ] = {
+ '7e03c745-7340-49ef-8af1-b8f7fe431c8a': (
+ "BTC/EUR", "order_type", "STOP_LIMIT", # sell at a lower price
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.SELL.value, False
+ ),
+ '1e2f0918-5728-4c68-b8f4-6fd804396248': (
+ "ETH/BTC", "order_type", "STOP_LIMIT", # buy at a higher price
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.BUY.value, True
+ ),
+ 'f4016e50-1f0b-4caa-abe5-1ec00af18be9': (
+ "ETH/BTC", "order_type", "STOP_LIMIT", # buy at a lower price
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.BUY.value, False
+ ),
+ } # stop loss / take profit and other special order types to be successfully parsed
+ # details of an order that exists but can"t be cancelled
+ UNCANCELLABLE_ORDER_ID_SYMBOL_TYPE: tuple[str, str, octobot_trading.enums.TraderOrderType] = (
+ "f4016e50-1f0b-4caa-abe5-1ec00af18be9", "ETH/BTC", octobot_trading.enums.TraderOrderType.BUY_LIMIT.value
+ )
+
async def test_get_portfolio(self):
await super().test_get_portfolio()
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
await super().test_get_account_id()
@@ -64,6 +96,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_coinex.py b/additional_tests/exchanges_tests/test_coinex.py
index d4fa0a9c1..8fa9c5abe 100644
--- a/additional_tests/exchanges_tests/test_coinex.py
+++ b/additional_tests/exchanges_tests/test_coinex.py
@@ -39,6 +39,9 @@ async def test_get_portfolio(self):
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -62,6 +65,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_cryptocom.py b/additional_tests/exchanges_tests/test_cryptocom.py
index 7963937b9..23ca404de 100644
--- a/additional_tests/exchanges_tests/test_cryptocom.py
+++ b/additional_tests/exchanges_tests/test_cryptocom.py
@@ -40,6 +40,9 @@ async def test_get_portfolio_with_market_filter(self):
# pass if not implemented
pass
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -63,6 +66,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_gateio.py b/additional_tests/exchanges_tests/test_gateio.py
index d5cf41b7e..009813fee 100644
--- a/additional_tests/exchanges_tests/test_gateio.py
+++ b/additional_tests/exchanges_tests/test_gateio.py
@@ -41,6 +41,9 @@ async def test_get_portfolio(self):
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -65,6 +68,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_hollaex.py b/additional_tests/exchanges_tests/test_hollaex.py
index be63620b9..07f31c107 100644
--- a/additional_tests/exchanges_tests/test_hollaex.py
+++ b/additional_tests/exchanges_tests/test_hollaex.py
@@ -44,6 +44,9 @@ async def test_get_portfolio(self):
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
await super().test_get_account_id()
@@ -65,6 +68,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_htx.py b/additional_tests/exchanges_tests/test_htx.py
index a22c17725..43923ff28 100644
--- a/additional_tests/exchanges_tests/test_htx.py
+++ b/additional_tests/exchanges_tests/test_htx.py
@@ -41,6 +41,9 @@ async def test_get_portfolio_with_market_filter(self):
# pass if not implemented
pass
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -64,6 +67,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_kucoin.py b/additional_tests/exchanges_tests/test_kucoin.py
index ef48cfbe7..3295f5392 100644
--- a/additional_tests/exchanges_tests/test_kucoin.py
+++ b/additional_tests/exchanges_tests/test_kucoin.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public
# License along with OctoBot. If not, see .
import pytest
+import octobot_trading.enums
from additional_tests.exchanges_tests import abstract_authenticated_exchange_tester
@@ -36,15 +37,53 @@ class TestKucoinAuthenticatedExchange(
VALID_ORDER_ID = "6617e84c5c1e0000083c71f7"
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented
+ SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID: dict[
+ str, (
+ str, # symbol
+ str, # order type key in 'info' dict
+ str, # order type found in 'info' dict
+ str, # parsed trading_enums.TradeOrderType
+ str, # parsed trading_enums.TradeOrderSide
+ bool, # trigger above (on higher price than order price)
+ )
+ ] = {
+ "vs93gpruc6ikekiv003o48ci": (
+ "BTC/USDT", "type", "market",
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.BUY.value, False
+ ),
+ 'vs93gpruc6n45taf003lr546': (
+ "BTC/USDT", "type", "market",
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.BUY.value, True
+ ),
+ 'vs93gpruc69s00cs003tat0g': (
+ "BTC/USDT", "type", "limit",
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.BUY.value, False
+ ),
+ 'vs93gpruc5q45taf003lr545': (
+ "BTC/USDT", "type", "limit",
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.BUY.value, True
+ ),
+ } # stop loss / take profit and other special order types to be successfully parsed
+ # details of an order that exists but can"t be cancelled
+ UNCANCELLABLE_ORDER_ID_SYMBOL_TYPE: tuple[str, str, octobot_trading.enums.TraderOrderType] = (
+ "vs93gpruc69s00cs003tat0g", "BTC/USDT", octobot_trading.enums.TraderOrderType.BUY_LIMIT.value
+ )
+
async def test_get_portfolio(self):
await super().test_get_portfolio()
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_kucoin_futures.py b/additional_tests/exchanges_tests/test_kucoin_futures.py
index a26b66a5d..8727b6f9e 100644
--- a/additional_tests/exchanges_tests/test_kucoin_futures.py
+++ b/additional_tests/exchanges_tests/test_kucoin_futures.py
@@ -15,6 +15,7 @@
# License along with OctoBot. If not, see .
import pytest
+import octobot_trading.enums
from additional_tests.exchanges_tests import abstract_authenticated_future_exchange_tester
# All test coroutines will be treated as marked.
@@ -33,7 +34,6 @@ class TestKucoinFuturesAuthenticatedExchange(
INVERSE_SYMBOL = f"{ORDER_CURRENCY}/USD:{ORDER_CURRENCY}"
ORDER_SIZE = 5 # % of portfolio to include in test orders
SUPPORTS_GET_LEVERAGE = False
- SUPPORTS_SET_LEVERAGE = False
USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS = True
VALID_ORDER_ID = "6617e84c5c1e0000083c71f7"
EXPECT_MISSING_FEE_IN_CANCELLED_ORDERS = False
@@ -41,12 +41,47 @@ class TestKucoinFuturesAuthenticatedExchange(
EXPECTED_QUOTE_MIN_ORDER_SIZE = 40
EXPECT_BALANCE_FILTER_BY_MARKET_STATUS = True
+ SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID: dict[
+ str, (
+ str, # symbol
+ str, # order type key in 'info' dict
+ str, # order type found in 'info' dict
+ str, # parsed trading_enums.TradeOrderType
+ str, # parsed trading_enums.TradeOrderSide
+ bool, # trigger above (on higher price than order price)
+ )
+ ] = {
+ "266424660906831872": (
+ "ETH/USDT:USDT", "type", "market",
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.BUY.value, False
+ ),
+ '266424746172764160': (
+ "ETH/USDT:USDT", "type", "market",
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.SELL.value, False
+ ),
+ '266424798085668865': (
+ "ETH/USDT:USDT", "type", "limit",
+ octobot_trading.enums.TradeOrderType.LIMIT.value, octobot_trading.enums.TradeOrderSide.BUY.value, False
+ ),
+ '266424826044899328': (
+ "ETH/USDT:USDT", "type", "limit",
+ octobot_trading.enums.TradeOrderType.STOP_LOSS.value, octobot_trading.enums.TradeOrderSide.SELL.value, False
+ ),
+ } # stop loss / take profit and other special order types to be successfully parsed
+ # details of an order that exists but can"t be cancelled
+ UNCANCELLABLE_ORDER_ID_SYMBOL_TYPE: tuple[str, str, octobot_trading.enums.TraderOrderType] = (
+ "266424798085668865", "ETH/USDT:USDT", octobot_trading.enums.TraderOrderType.BUY_LIMIT.value
+ )
+
async def test_get_portfolio(self):
await super().test_get_portfolio()
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter() # can have small variations failing the test when positions are open
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
await super().test_get_account_id()
@@ -77,6 +112,9 @@ async def test_get_and_set_leverage(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
# todo test cross position order creation (kucoin param) at next ccxt update (will support set margin type)
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_mexc.py b/additional_tests/exchanges_tests/test_mexc.py
index a47659703..14975241b 100644
--- a/additional_tests/exchanges_tests/test_mexc.py
+++ b/additional_tests/exchanges_tests/test_mexc.py
@@ -28,12 +28,15 @@ class TestMEXCAuthenticatedExchange(
EXCHANGE_NAME = "mexc"
EXCHANGE_TENTACLE_NAME = "MEXC"
ORDER_CURRENCY = "BTC"
- SETTLEMENT_CURRENCY = "USDC"
+ SETTLEMENT_CURRENCY = "USDT"
SYMBOL = f"{ORDER_CURRENCY}/{SETTLEMENT_CURRENCY}"
ORDER_SIZE = 30 # % of portfolio to include in test orders
CONVERTS_ORDER_SIZE_BEFORE_PUSHING_TO_EXCHANGES = True
CANCELLED_ORDERS_IN_CLOSED_ORDERS = True
EXPECT_MISSING_FEE_IN_CANCELLED_ORDERS = False
+ IS_ACCOUNT_ID_AVAILABLE = False
+ USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS = True
+ USED_TO_HAVE_UNTRADABLE_SYMBOL = True
async def test_get_portfolio(self):
await super().test_get_portfolio()
@@ -41,9 +44,11 @@ async def test_get_portfolio(self):
async def test_get_portfolio_with_market_filter(self):
await super().test_get_portfolio_with_market_filter()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
- # pass if not implemented
- pass
+ await super().test_get_account_id()
async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()
@@ -52,11 +57,10 @@ 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
+ await super().test_get_api_key_permissions()
async def test_missing_trading_api_key_permissions(self):
- pass
+ await super().test_missing_trading_api_key_permissions()
async def test_get_not_found_order(self):
await super().test_get_not_found_order()
@@ -64,6 +68,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
@@ -80,7 +87,7 @@ async def test_get_cancelled_orders(self):
await super().test_get_cancelled_orders()
async def test_create_and_cancel_stop_orders(self):
- # pass if not implemented
+ # NOT SUPPORTED by MEXC API (10/01/25)
pass
async def test_edit_limit_order(self):
diff --git a/additional_tests/exchanges_tests/test_okx.py b/additional_tests/exchanges_tests/test_okx.py
index 632cceaf2..db4f85c1c 100644
--- a/additional_tests/exchanges_tests/test_okx.py
+++ b/additional_tests/exchanges_tests/test_okx.py
@@ -45,12 +45,18 @@ async def test_get_portfolio_with_market_filter(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
async def test_get_account_id(self):
await super().test_get_account_id()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()
diff --git a/additional_tests/exchanges_tests/test_okx_futures.py b/additional_tests/exchanges_tests/test_okx_futures.py
index 656efb7a9..fd47c3936 100644
--- a/additional_tests/exchanges_tests/test_okx_futures.py
+++ b/additional_tests/exchanges_tests/test_okx_futures.py
@@ -46,6 +46,9 @@ async def test_get_portfolio_with_market_filter(self):
async def test_get_account_id(self):
await super().test_get_account_id()
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()
@@ -73,6 +76,9 @@ async def test_get_and_set_leverage(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/exchanges_tests/test_phemex.py b/additional_tests/exchanges_tests/test_phemex.py
index b570e3bd8..6d2ba7f84 100644
--- a/additional_tests/exchanges_tests/test_phemex.py
+++ b/additional_tests/exchanges_tests/test_phemex.py
@@ -39,6 +39,9 @@ async def test_get_portfolio_with_market_filter(self):
# pass if not implemented
pass
+ async def test_untradable_symbols(self):
+ await super().test_untradable_symbols()
+
async def test_get_account_id(self):
# pass if not implemented
pass
@@ -62,6 +65,9 @@ async def test_get_not_found_order(self):
async def test_is_valid_account(self):
await super().test_is_valid_account()
+ async def test_get_special_orders(self):
+ await super().test_get_special_orders()
+
async def test_create_and_cancel_limit_orders(self):
await super().test_create_and_cancel_limit_orders()
diff --git a/additional_tests/supabase_backend_tests/test_storage.py b/additional_tests/supabase_backend_tests/test_storage.py
index ab73b3b2f..45f373f13 100644
--- a/additional_tests/supabase_backend_tests/test_storage.py
+++ b/additional_tests/supabase_backend_tests/test_storage.py
@@ -28,14 +28,22 @@ async def test_upload_asset(admin_client):
asset_name = "test_upload_asset"
asset_bucket = "product-images"
await admin_client.remove_asset(asset_bucket, asset_name) # remove asset if exists
- uploaded_asset_id = await admin_client.upload_asset(asset_bucket, asset_name, asset_content)
+ uploaded_asset_path = await admin_client.upload_asset(asset_bucket, asset_name, asset_content)
assets = await admin_client.list_assets(asset_bucket)
- asset_by_id = {
- asset["id"]: asset
+ asset_by_name = {
+ asset["name"]: asset
for asset in assets
}
- assert uploaded_asset_id in asset_by_id
- assert asset_by_id[uploaded_asset_id]["name"] == asset_name
+ assert uploaded_asset_path in asset_by_name
+ assert asset_by_name[uploaded_asset_path]["name"] == asset_name
await admin_client.remove_asset(asset_bucket, asset_name)
+
+ assets = await admin_client.list_assets(asset_bucket)
+ asset_by_name = {
+ asset["name"]: asset
+ for asset in assets
+ }
+ # asset is removed
+ assert uploaded_asset_path not in asset_by_name
diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py
index 3b40136d4..523e88104 100644
--- a/octobot/community/authentication.py
+++ b/octobot/community/authentication.py
@@ -607,6 +607,8 @@ async def _initialize_account(self, minimal=False, fetch_private_data=True):
await self._ensure_init_community_feed()
except authentication.AuthenticationError as err:
self.logger.info(f"Login aborted: no authenticated session: {err}")
+ if await self.has_login_info():
+ await self.logout()
except authentication.UnavailableError as e:
self.logger.exception(e, True, f"Error when fetching community data, "
f"please check your internet connection.")
@@ -803,7 +805,8 @@ async def _auth_handler(self):
except authentication.FailedAuthentication as e:
if should_warn:
self.logger.warning(f"Invalid authentication details, please re-authenticate. {e}")
- await self.logout()
+ if await self.has_login_info():
+ await self.logout()
except authentication.UnavailableError:
raise
except Exception as e:
diff --git a/octobot/community/errors_upload/sentry_aiohttp_transport.py b/octobot/community/errors_upload/sentry_aiohttp_transport.py
index 1a77a4db9..83ddcaf5f 100644
--- a/octobot/community/errors_upload/sentry_aiohttp_transport.py
+++ b/octobot/community/errors_upload/sentry_aiohttp_transport.py
@@ -17,8 +17,6 @@
import typing
import asyncio
import aiohttp
-import io
-import gzip
import sentry_sdk
import sentry_sdk.consts
@@ -33,6 +31,8 @@ def __init__(
self, options: typing.Dict[str, typing.Any]
):
super().__init__(options)
+ # WARNING: override default "br" value: not supported by Glitchtip yet
+ self._compression_algo = "gzip"
# use custom async worker instead of default sentry thread worker
# does not support proxies, at least for now
self._worker = AiohttpWorker(queue_size=options["transport_queue_size"])
@@ -75,7 +75,7 @@ def record_loss(reason: str) -> None:
pass
elif response.status >= 300 or response.status < 200:
- sentry_sdk.utils.logger.error(
+ logging.getLogger(self.__class__.__name__).warning(
"Unexpected status code: %s (body: %s)",
response.status,
await response.text(),
@@ -121,14 +121,7 @@ async def _async_send_envelope(
if client_report_item is not None:
envelope.items.append(client_report_item)
- body = io.BytesIO()
- if self._compresslevel == 0:
- envelope.serialize_into(body)
- else:
- with gzip.GzipFile(
- fileobj=body, mode="w", compresslevel=self._compresslevel
- ) as f:
- envelope.serialize_into(f)
+ content_encoding, body = self._serialize_envelope(envelope)
assert self.parsed_dsn is not None
sentry_sdk.utils.logger.debug(
@@ -141,8 +134,8 @@ async def _async_send_envelope(
headers = {
"Content-Type": "application/x-sentry-envelope",
}
- if self._compresslevel > 0:
- headers["Content-Encoding"] = "gzip"
+ if content_encoding:
+ headers["Content-Encoding"] = content_encoding
await self._async_send_request(
body.getvalue(),
@@ -163,13 +156,11 @@ def capture_event(
def capture_envelope(
self, envelope: sentry_sdk.envelope.Envelope
) -> None:
- hub = self.hub_cls.current
async def send_envelope_wrapper() -> None:
- with hub: # pylint: disable=not-context-manager
- with sentry_sdk.utils.capture_internal_exceptions():
- await self._async_send_envelope(envelope)
- self._flush_client_reports()
+ with sentry_sdk.utils.capture_internal_exceptions():
+ await self._async_send_envelope(envelope)
+ self._flush_client_reports()
if not self._worker.submit(send_envelope_wrapper):
self.on_dropped_event("full_queue")
@@ -225,8 +216,7 @@ def full(self) -> bool:
return len(self.call_tasks) > self._queue_size
def flush(self, timeout: float, callback=None) -> None:
- sentry_sdk.utils.logger.debug("background worker got flush request")
- sentry_sdk.utils.logger.debug("background worker flush ignored")
+ sentry_sdk.utils.logger.debug("Custom background worker got flush request, ignored")
async def _async_call(self, callback):
try:
diff --git a/octobot/community/models/community_public_data.py b/octobot/community/models/community_public_data.py
index bb009dd99..3d289c527 100644
--- a/octobot/community/models/community_public_data.py
+++ b/octobot/community/models/community_public_data.py
@@ -23,8 +23,9 @@ def __init__(self):
self.products = _DataElement({}, False)
def set_products(self, products):
- self.products.value = {product[enums.ProductKeys.ID.value]: product for product in products}
- self.products.fetched = True
+ if products:
+ self.products.value = {product[enums.ProductKeys.ID.value]: product for product in products}
+ self.products.fetched = True
def get_product_slug(self, product_id):
return self.products.value[product_id][enums.ProductKeys.SLUG.value]
diff --git a/octobot/community/models/formatters.py b/octobot/community/models/formatters.py
index e7bff3f88..092029c14 100644
--- a/octobot/community/models/formatters.py
+++ b/octobot/community/models/formatters.py
@@ -123,6 +123,9 @@ def format_orders(orders: list, exchange_name: str) -> list:
trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value],
backend_enums.OrderKeys.SIDE.value: storage_order[trading_constants.STORAGE_ORIGIN_VALUE][
trading_enums.ExchangeConstantsOrderColumns.SIDE.value],
+ backend_enums.OrderKeys.TRIGGER_ABOVE.value: storage_order[trading_constants.STORAGE_ORIGIN_VALUE].get(
+ trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value
+ ),
backend_enums.OrderKeys.EXCHANGE_ID.value: storage_order[trading_constants.STORAGE_ORIGIN_VALUE][
trading_enums.ExchangeConstantsOrderColumns.EXCHANGE_ID.value],
backend_enums.OrderKeys.CHAINED.value: format_orders(
diff --git a/octobot/community/supabase_backend/community_supabase_client.py b/octobot/community/supabase_backend/community_supabase_client.py
index 3e15042f3..ceac74dc0 100644
--- a/octobot/community/supabase_backend/community_supabase_client.py
+++ b/octobot/community/supabase_backend/community_supabase_client.py
@@ -122,7 +122,7 @@ async def sign_up(self, email: str, password: str) -> None:
async def sign_out(self, options: gotrue.types.SignOutOptions) -> None:
try:
await self.auth.sign_out(options)
- except gotrue.errors.AuthApiError:
+ except (postgrest.exceptions.APIError, gotrue.errors.AuthApiError):
pass
def _requires_email_validation(self, user: gotrue.types.User) -> bool:
@@ -137,7 +137,7 @@ async def restore_session(self):
async def refresh_session(self, refresh_token: typing.Union[str, None] = None):
try:
await self.auth.refresh_session(refresh_token=refresh_token)
- except gotrue.errors.AuthError as err:
+ except (postgrest.exceptions.APIError, gotrue.errors.AuthError) as err:
raise authentication.AuthenticationError(f"Community auth error: {err}") from err
async def sign_in_with_otp_token(self, token):
@@ -229,16 +229,21 @@ async def fetch_checkout_url(self, payment_method: str, redirect_url: str) -> di
return json.loads(json.loads(resp)["message"])
async def fetch_bot(self, bot_id) -> dict:
- try:
- # https://postgrest.org/en/stable/references/api/resource_embedding.html#hint-disambiguation
- return (await self.table("bots").select("*,bot_deployment:bot_deployments!bots_current_deployment_id_fkey(*)").eq(
- enums.BotKeys.ID.value, bot_id
- ).execute()).data[0]
- except IndexError:
- raise errors.BotNotFoundError(f"Can't find bot with id: {bot_id}")
+ with jwt_expired_auth_raiser():
+ try:
+ # https://postgrest.org/en/stable/references/api/resource_embedding.html#hint-disambiguation
+ return (await self.table("bots").select("*,bot_deployment:bot_deployments!bots_current_deployment_id_fkey(*)").eq(
+ enums.BotKeys.ID.value, bot_id
+ ).execute()).data[0]
+ except IndexError:
+ raise errors.BotNotFoundError(f"Can't find bot with id: {bot_id}")
async def fetch_bots(self) -> list:
- return (await self.table("bots").select("*,bot_deployment:bot_deployments!bots_current_deployment_id_fkey!inner(*)").execute()).data
+ with jwt_expired_auth_raiser():
+ return (
+ await self.table("bots").select(
+ "*,bot_deployment:bot_deployments!bots_current_deployment_id_fkey!inner(*)"
+ ).execute()).data
async def create_bot(self, deployment_type: enums.DeploymentTypes) -> dict:
created_bot = (await self.table("bots").insert({
@@ -305,19 +310,23 @@ async def fetch_startup_info(self, bot_id) -> dict:
return resp.data[0]
async def fetch_products(self, category_types: list[str]) -> list:
- return (
- await self.table("products").select(
- "*,"
- "category:product_categories!inner(slug, name_translations, type, metadata),"
- "results:product_results!products_current_result_id_fkey("
- " profitability,"
- " reference_market_profitability"
- ")"
- ).eq(
- enums.ProductKeys.VISIBILITY.value, "public"
- ).in_("category.type", category_types)
- .execute()
- ).data
+ try:
+ return (
+ await self.table("products").select(
+ "*,"
+ "category:product_categories!inner(slug, name_translations, type, metadata),"
+ "results:product_results!products_current_result_id_fkey("
+ " profitability,"
+ " reference_market_profitability"
+ ")"
+ ).eq(
+ enums.ProductKeys.VISIBILITY.value, "public"
+ ).in_("category.type", category_types)
+ .execute()
+ ).data
+ except postgrest.exceptions.APIError as err:
+ commons_logging.get_logger(__name__).error(f"Error when fetching products: {err}")
+ return []
async def fetch_subscribed_products_urls(self) -> list:
resp = await self.rpc("get_subscribed_products_urls").execute()
@@ -428,9 +437,10 @@ async def fetch_bot_profile_data(self, bot_config_id: str) -> commons_profiles.P
")"
).eq(enums.BotConfigKeys.ID.value, bot_config_id).execute()).data[0]
try:
- profile_data = commons_profiles.ProfileData.from_dict(
- bot_config["product_config"][enums.ProfileConfigKeys.CONFIG.value]
- )
+ profile_config = bot_config["product_config"][enums.ProfileConfigKeys.CONFIG.value]
+ if not profile_config:
+ raise TypeError(f"product_config.config is '{profile_config}'")
+ profile_data = commons_profiles.ProfileData.from_dict(profile_config)
except (TypeError, KeyError) as err:
raise errors.InvalidBotConfigError(f"Missing bot product config: {err} ({err.__class__.__name__})") from err
profile_data.profile_details.name = bot_config["product_config"].get("product", {}).get(
@@ -862,7 +872,7 @@ async def upload_asset(self, bucket_name: str, asset_name: str, content: typing.
Not implemented for authenticated users
"""
result = await self.storage.from_(bucket_name).upload(asset_name, content)
- return result.json()["Id"]
+ return result.path
async def list_assets(self, bucket_name: str) -> list[dict[str, str]]:
"""
@@ -870,11 +880,11 @@ async def list_assets(self, bucket_name: str) -> list[dict[str, str]]:
"""
return await self.storage.from_(bucket_name).list()
- async def remove_asset(self, bucket_name: str, asset_name: str) -> None:
+ async def remove_asset(self, bucket_name: str, asset_path: str) -> None:
"""
Not implemented for authenticated users
"""
- await self.storage.from_(bucket_name).remove(asset_name)
+ await self.storage.from_(bucket_name).remove([asset_path])
async def send_signal(self, table, product_id: str, signal: str):
return (await self.table(table).insert({
@@ -934,4 +944,14 @@ async def aclose(self):
except RuntimeError:
# happens when the event loop is closed already
pass
- self.production_anon_client = None
\ No newline at end of file
+ self.production_anon_client = None
+
+
+@contextlib.contextmanager
+def jwt_expired_auth_raiser():
+ try:
+ yield
+ except postgrest.exceptions.APIError as err:
+ if "JWT expired" in str(err):
+ raise authentication.AuthenticationError(f"Please re-login to your OctoBot account: {err}") from err
+ raise
diff --git a/octobot/community/supabase_backend/enums.py b/octobot/community/supabase_backend/enums.py
index 2c7dcfa84..c421db22c 100644
--- a/octobot/community/supabase_backend/enums.py
+++ b/octobot/community/supabase_backend/enums.py
@@ -173,6 +173,7 @@ class OrderKeys(enum.Enum):
TYPE = "type"
CHAINED = "chained"
SIDE = "side"
+ TRIGGER_ABOVE = "trigger_above"
class PositionKeys(enum.Enum):
diff --git a/octobot/constants.py b/octobot/constants.py
index 7e0108ad3..97e25675f 100644
--- a/octobot/constants.py
+++ b/octobot/constants.py
@@ -111,7 +111,7 @@
USE_FETCHED_BOT_CONFIG = os_util.parse_boolean_environment_var("USE_FETCHED_BOT_CONFIG", "false")
CAN_INSTALL_TENTACLES = os_util.parse_boolean_environment_var("CAN_INSTALL_TENTACLES", str(not IS_CLOUD_ENV))
TRACKING_ID = os.getenv("TRACKING_ID", "eoe1stwyun" if IS_DEMO else "eoe06soct7" if IS_CLOUD_ENV else "f726lk9q59")
-PH_TRACKING_ID = os.getenv("PH_TRACKING_ID", "phc_VydQbPkMXoNhgd0xJde4hUgbWGlEJ3aaLrSu5sudFdJ")
+PH_TRACKING_ID = os.getenv("PH_TRACKING_ID", "phc_QSuFy6zqOXXKT7zAYboYS4nJShfKovpB172aa8X9nXf")
# Profiles download urls to import at startup if missing, split by ","
TO_DOWNLOAD_PROFILES = os.getenv("TO_DOWNLOAD_PROFILES", None)
# Profiles to force select at startup, identified by profile id, download url or name
diff --git a/requirements.txt b/requirements.txt
index b5d646de9..2e55c85de 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,12 +1,12 @@
# Drakkar-Software requirements
OctoBot-Commons==1.9.70
-OctoBot-Trading==2.4.140
+OctoBot-Trading==2.4.147
OctoBot-Evaluators==1.9.7
OctoBot-Tentacles-Manager==2.9.16
-OctoBot-Services==1.6.21
+OctoBot-Services==1.6.23
OctoBot-Backtesting==1.9.7
Async-Channel==2.2.1
-trading-backend==1.2.30
+trading-backend==1.2.32
## Others
colorlog==6.8.0
@@ -18,14 +18,14 @@ setuptools==69.0.3
# Community
websockets
-gmqtt==0.6.16
+gmqtt==0.7.0
pgpy==0.6.0
# Error tracking
-sentry-sdk==2.13.0 # always make sure sentry_aiohttp_transport.py keep working
+sentry-sdk==2.19.2 # always make sure sentry_aiohttp_transport.py keep working
# Supabase ensure supabase_backend_tests keep passing when updating any of those
-supabase==2.7.1 # Supabase client
+supabase==2.11.0 # Supabase client
gotrue # Supabase authenticated API (required by supabase and enforced to allow direct import)
postgrest # Supabase posgres calls (required by supabase and enforced to allow direct import)
diff --git a/tests/unit_tests/community/errors_upload/test_sentry_aiohttp_transport.py b/tests/unit_tests/community/errors_upload/test_sentry_aiohttp_transport.py
index eca475a29..b311b2191 100644
--- a/tests/unit_tests/community/errors_upload/test_sentry_aiohttp_transport.py
+++ b/tests/unit_tests/community/errors_upload/test_sentry_aiohttp_transport.py
@@ -61,9 +61,6 @@ def _before_send_callback(event: dict, hint: dict):
SENTRY_CONFIG["enabled"] = False
if handle and hasattr(handle._client.transport, "async_kill"):
await handle._client.transport.async_kill()
- client = sentry_sdk.Hub.current.client
- if client is not None:
- client.close(timeout=0)
def _mocked_context(return_value):
diff --git a/tests/unit_tests/community/test_community_mqtt_feed.py b/tests/unit_tests/community/test_community_mqtt_feed.py
index 52e3be0c5..1feb8a371 100644
--- a/tests/unit_tests/community/test_community_mqtt_feed.py
+++ b/tests/unit_tests/community/test_community_mqtt_feed.py
@@ -69,8 +69,6 @@ async def connected_community_feed(authenticator):
finally:
if feed is not None:
await feed.stop()
- if feed._mqtt_client is not None and not feed._mqtt_client._resend_task.done():
- feed._mqtt_client._resend_task.cancel()
async def test_start_and_connect(connected_community_feed):