Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ async def cancel_order(
try:
# make sure order is canceled
cancelled_order = await self.exchange_manager.exchange.get_order(
exchange_order_id, symbol=symbol
exchange_order_id, symbol=symbol, order_type=order_type
)
if cancelled_order is None or personal_data.parse_is_cancelled(cancelled_order):
return enums.OrderStatus.CANCELED
Expand Down
4 changes: 4 additions & 0 deletions octobot_trading/exchanges/connectors/ccxt/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,7 @@ class ExchangeColumns(enum.Enum):
class ExchangeMarginTypes(enum.Enum):
ISOLATED = "isolated"
CROSS = "cross"


class OrderFetchParams(enum.Enum):
STOP = "stop" # bool: when true, is about a stop order
78 changes: 57 additions & 21 deletions octobot_trading/exchanges/types/rest_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@
import octobot_trading.personal_data.orders as orders



def fetching_orders_request(f):
async def fetching_orders_request_wrapper(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list:
fetched_orders = await f(self, symbol=symbol, since=since, limit=limit, **kwargs)
if ccxt_enums.OrderFetchParams.STOP.value not in kwargs and self.fetch_stop_order_in_different_request():
# all order types need to be fetched and stop orders need to be fetched in a separate request
kwargs[ccxt_enums.OrderFetchParams.STOP.value] = True
fetched_orders.extend(
await f(self, symbol=symbol, since=since, limit=limit, **kwargs)
)
return await self._ensure_orders_completeness(
fetched_orders, symbol, since=since, limit=limit, **kwargs
)
return fetching_orders_request_wrapper



class RestExchange(abstract_exchange.AbstractExchange):
ORDER_NON_EMPTY_FIELDS = [ecoc.EXCHANGE_ID.value, ecoc.TIMESTAMP.value, ecoc.SYMBOL.value, ecoc.TYPE.value,
ecoc.SIDE.value, ecoc.PRICE.value, ecoc.AMOUNT.value, ecoc.STATUS.value]
Expand Down Expand Up @@ -232,6 +249,10 @@ def get_adapter_class(self):
# Override in tentacles when using a custom adapter
return None

def fetch_stop_order_in_different_request(self) -> bool:
# Override in tentacles when stop orders need to be fetched in a separate request from CCXT
return False

async def create_order(self, order_type: enums.TraderOrderType, symbol: str, quantity: decimal.Decimal,
price: decimal.Decimal = None, stop_price: decimal.Decimal = None,
side: enums.TradeOrderSide = None, current_price: decimal.Decimal = None,
Expand Down Expand Up @@ -370,9 +391,12 @@ async def _verify_order(self, created_order, order_type, symbol, price, quantity
if order_exchange_id is None:
self.logger.error(f"No order exchange id on created order: {created_order}")
return None
params = get_order_params or {}
exchange_order_id = created_order[ecoc.EXCHANGE_ID.value]
params = self._order_request_kwargs_factory(
exchange_order_id, order_type, **(get_order_params or {})
)
fetched_order = await self.get_order(
created_order[ecoc.EXCHANGE_ID.value], symbol=symbol, **params
exchange_order_id, symbol=symbol, **params
)
if fetched_order is None:
created_order[ecoc.STATUS.value] = enums.OrderStatus.PENDING_CREATION.value
Expand Down Expand Up @@ -688,9 +712,25 @@ async def get_price_ticker(self, symbol: str, **kwargs: dict) -> typing.Optional
async def get_all_currencies_price_ticker(self, **kwargs: dict) -> typing.Optional[dict[str, dict]]:
return await self.connector.get_all_currencies_price_ticker(**kwargs)

async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
def _order_request_kwargs_factory(
self,
exchange_order_id: str,
order_type: typing.Optional[enums.TraderOrderType] = None,
**kwargs
) -> dict:
# implement if the exchange needs additional kwargs to fetch an order, like to fetch stop orders
return kwargs

async def get_order(
self,
exchange_order_id: str,
symbol: typing.Optional[str] = None,
order_type: typing.Optional[enums.TraderOrderType] = None,
**kwargs: dict
) -> dict:
extended_kwargs = self._order_request_kwargs_factory(exchange_order_id, order_type, **(kwargs or {}))
return await self._ensure_order_completeness(
await self.connector.get_order(exchange_order_id, symbol=symbol, **kwargs),
await self.connector.get_order(exchange_order_id, symbol=symbol, **extended_kwargs),
symbol, **kwargs
)

Expand All @@ -713,39 +753,34 @@ async def get_order_from_trades(self, symbol, exchange_order_id, order_to_update
return None #OrderNotFound

async def get_all_orders(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list:
return await self._ensure_orders_completeness(
await self.connector.get_all_orders(symbol=symbol, since=since, limit=limit, **kwargs),
symbol, since=since, limit=limit, **kwargs
)
return await self.connector.get_all_orders(symbol=symbol, since=since, limit=limit, **kwargs)

@fetching_orders_request
async def get_open_orders(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list:
return await self._ensure_orders_completeness(
await self.connector.get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs),
symbol, since=since, limit=limit, **kwargs
)
return await self.connector.get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)

@fetching_orders_request
async def _get_closed_orders(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list:
return await self.connector.get_closed_orders(symbol=symbol, since=since, limit=limit, **kwargs)

async def get_closed_orders(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list:
# uses connector.get_closed_orders if supported, otherwise uses recent trades
try:
return await self._ensure_orders_completeness(
await self.connector.get_closed_orders(symbol=symbol, since=since, limit=limit, **kwargs),
symbol, since=since, limit=limit, **kwargs
)
return await self._get_closed_orders(symbol=symbol, since=since, limit=limit, **kwargs)
except errors.NotSupported:
if self.REQUIRE_CLOSED_ORDERS_FROM_RECENT_TRADES:
return await self._get_closed_orders_from_my_recent_trades(
symbol=symbol, since=since, limit=limit, **kwargs
)
raise

@fetching_orders_request
async def get_cancelled_orders(
self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict
) -> list:
if not self.SUPPORT_FETCHING_CANCELLED_ORDERS:
raise errors.NotSupported(f"get_cancelled_orders is not supported")
return await self._ensure_orders_completeness(
await self.connector.get_cancelled_orders(symbol=symbol, since=since, limit=limit, **kwargs),
symbol, since=since, limit=limit, **kwargs
)
return await self.connector.get_cancelled_orders(symbol=symbol, since=since, limit=limit, **kwargs)

async def _get_closed_orders_from_my_recent_trades(
self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict
Expand Down Expand Up @@ -806,7 +841,8 @@ async def cancel_all_orders(self, symbol: str = None, **kwargs: dict) -> None:
async def cancel_order(
self, exchange_order_id: str, symbol: str, order_type: enums.TraderOrderType, **kwargs: dict
) -> enums.OrderStatus:
return await self.connector.cancel_order(exchange_order_id, symbol, order_type, **kwargs)
extended_kwargs = self._order_request_kwargs_factory(exchange_order_id, order_type, **(kwargs or {}))
return await self.connector.cancel_order(exchange_order_id, symbol, order_type, **extended_kwargs)

def get_trade_fee(self, symbol: str, order_type: enums.TraderOrderType, quantity, price, taker_or_maker) -> dict:
return self.connector.get_trade_fee(symbol, order_type, quantity, price, taker_or_maker)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,9 @@ async def _order_fetch_and_push(self, order, should_notify=False):
"""
exchange_name = self.channel.exchange_manager.exchange_name
self.logger.info(f"Requested update for {order} on {exchange_name}")
raw_order = await self.channel.exchange_manager.exchange.get_order(order.exchange_order_id, order.symbol)
raw_order = await self.channel.exchange_manager.exchange.get_order(
order.exchange_order_id, order.symbol, order_type=order.order_type
)

if raw_order is not None:
self.logger.info(f"Received update for {order} on {exchange_name}: {raw_order}")
Expand Down
4 changes: 3 additions & 1 deletion octobot_trading/personal_data/orders/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,9 @@ def get_profitability(self):
return self.order_profitability

async def default_exchange_update_order_status(self):
raw_order = await self.exchange_manager.exchange.get_order(self.exchange_order_id, self.symbol)
raw_order = await self.exchange_manager.exchange.get_order(
self.exchange_order_id, self.symbol, order_type=self.order_type
)
new_status = order_util.parse_order_status(raw_order)
self.is_synchronized_with_exchange = True
if new_status in {enums.OrderStatus.FILLED, enums.OrderStatus.CLOSED}:
Expand Down
9 changes: 7 additions & 2 deletions octobot_trading/util/test_tools/exchanges_test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,11 @@ async def get_order(
exchange_manager,
exchange_order_id: str,
symbol: str,
order_type: enums.TraderOrderType,
) -> typing.Optional[dict]:
return await exchange_manager.exchange.get_order(exchange_order_id, symbol=symbol)
return await exchange_manager.exchange.get_order(
exchange_order_id, symbol=symbol, order_type=order_type
)


@exchanges.retried_failed_network_request(
Expand Down Expand Up @@ -402,7 +405,9 @@ async def wait_for_other_status(order: personal_data.Order, timeout) -> personal
iterations = 0
origin_status = order.status.value
while time.time() - t0 < timeout:
raw_order = await order.exchange_manager.exchange.get_order(order.exchange_order_id, order.symbol)
raw_order = await order.exchange_manager.exchange.get_order(
order.exchange_order_id, order.symbol, order_type=order.order_type
)
iterations += 1
if raw_order is not None and raw_order[enums.ExchangeConstantsOrderColumns.STATUS.value] != origin_status:
logging.get_logger(order.get_logger_name()).info(
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ OctoBot-Tentacles-Manager>=2.9, <2.10
trading-backend>=1.2.35

# Exchange connection requirements
ccxt==4.5.22 # always ensure real exchanges tests (in tests_additional and authenticated exchange tests) are passing before changing the ccxt version
ccxt==4.5.28 # always ensure real exchanges tests (in tests_additional and authenticated exchange tests) are passing before changing the ccxt version

cryptography # Never specify a version (managed by https://github.com/Drakkar-Software/OctoBot-PyPi-Linux-Deployer)

Expand Down
17 changes: 15 additions & 2 deletions tests_additional/real_exchanges/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import contextlib
import dotenv
import os
import mock

import octobot_commons.constants as commons_constants
import octobot_commons.asyncio_tools as asyncio_tools
Expand All @@ -24,13 +25,14 @@
import octobot_trading.exchanges as exchanges
import octobot_trading.enums as enums
import octobot_trading.errors as errors
import octobot_trading.exchanges.util.exchange_util


LOADED_EXCHANGE_CREDS_ENV_VARIABLES = False


@contextlib.asynccontextmanager
async def get_exchange_manager(exchange_name, config=None, authenticated=False, market_filter=None):
async def get_exchange_manager(exchange_name, config=None, authenticated=False, market_filter=None, uses_tentacle=False):
config = {**test_config.load_test_config(), **config} if config else test_config.load_test_config()
if exchange_name not in config[commons_constants.CONFIG_EXCHANGES]:
config[commons_constants.CONFIG_EXCHANGES][exchange_name] = {}
Expand All @@ -43,7 +45,18 @@ async def get_exchange_manager(exchange_name, config=None, authenticated=False,
if config[commons_constants.CONFIG_EXCHANGES][exchange_name]. \
get(commons_constants.CONFIG_EXCHANGE_TYPE, enums.ExchangeTypes.SPOT.value) == enums.ExchangeTypes.FUTURE.value:
exchange_manager_instance.is_future = True
await exchange_manager_instance.initialize(exchange_config_by_exchange=None)
if not uses_tentacle:
# don't use exchange specific tentacles, even if they are available
with mock.patch.object(
octobot_trading.exchanges.util.exchange_util,
"search_exchange_class_from_exchange_name",
mock.Mock(return_value=exchanges.DefaultRestExchange)
) as search_exchange_class_from_exchange_name_mock:
await exchange_manager_instance.initialize(exchange_config_by_exchange=None)
assert search_exchange_class_from_exchange_name_mock.call_count > 0
else:
# allow the use of exchange specific tentacles
await exchange_manager_instance.initialize(exchange_config_by_exchange=None)
try:
yield exchange_manager_instance
except errors.UnreachableExchange as err:
Expand Down
8 changes: 5 additions & 3 deletions tests_additional/real_exchanges/real_exchange_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import pytest
from ccxt import Exchange

import ccxt
import octobot_commons.constants as constants
import octobot_commons.enums as commons_enums
import octobot_trading.enums as trading_enums
Expand Down Expand Up @@ -49,6 +49,7 @@ class RealExchangeTester:
HISTORICAL_CANDLES_TO_FETCH_COUNT = 650
DEFAULT_CUSTOM_BOOK_LIMIT = 50
ORDER_BOOK_DESYNC_ALLOWANCE = 60 # allow 60s desync
USES_TENTACLE = False # set True when an exchange tentacles should be used in this test

# Public methods: to be implemented as tests
# Use await self._[method_name] to get the test request result
Expand Down Expand Up @@ -135,7 +136,7 @@ async def test_get_historical_ohlcv(self):
self.HISTORICAL_CANDLES_TO_FETCH_COUNT * 0.85
< len(historical_ohlcv)
<= self.HISTORICAL_CANDLES_TO_FETCH_COUNT
)
), f"{len(historical_ohlcv)=} not in [{self.HISTORICAL_CANDLES_TO_FETCH_COUNT * 0.85}:{self.HISTORICAL_CANDLES_TO_FETCH_COUNT}]"
for candle in historical_ohlcv:
assert start <= candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value] <= end

Expand Down Expand Up @@ -263,7 +264,8 @@ def _ensure_book_custom_limit(
async def get_exchange_manager(self, market_filter=None):
async with get_exchange_manager(
self.EXCHANGE_NAME, config=self.get_config(),
authenticated=self.REQUIRES_AUTH, market_filter=market_filter
authenticated=self.REQUIRES_AUTH, market_filter=market_filter,
uses_tentacle=self.USES_TENTACLE
) as exchange_manager:
yield exchange_manager

Expand Down
1 change: 1 addition & 0 deletions tests_additional/real_exchanges/test_ascendex.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async def test_active_symbols(self):
async def test_get_market_status(self):
for market_status in await self.get_market_statuses():
self.ensure_required_market_status_values(market_status)
print(f"market_status: {market_status}")
# on AscendEx, precision is a decimal instead of a number of digits
assert 0 < market_status[Ecmsc.PRECISION.value][
Ecmsc.PRECISION_AMOUNT.value] <= 1 # to be fixed in AscendEx tentacle
Expand Down
2 changes: 1 addition & 1 deletion tests_additional/real_exchanges/test_htx.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def test_time_frames(self):
))

async def test_active_symbols(self):
await self.inner_test_active_symbols(900, 1900)
await self.inner_test_active_symbols(850, 1900)

async def test_get_market_status(self):
for market_status in await self.get_market_statuses():
Expand Down
4 changes: 2 additions & 2 deletions tests_additional/real_exchanges/test_hyperliquid.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ async def test_time_frames(self):
))

async def test_active_symbols(self):
await self.inner_test_active_symbols(450, 450)
await self.inner_test_active_symbols(400, 450)

async def test_get_market_status(self):
for market_status in await self.get_market_statuses():
Expand All @@ -83,7 +83,7 @@ async def test_get_market_status(self):
assert 0 < market_status[Ecmsc.PRECISION.value][
Ecmsc.PRECISION_PRICE.value] <= 1 # to be fixed in Hyperliquid tentacle
assert all(elem in market_status[Ecmsc.LIMITS.value]
for elem in (Ecmsc.LIMITS_AMOUNT.value,
for elem in (Ecmsc.LIMITS_AMOUNT.value,
Ecmsc.LIMITS_PRICE.value,
Ecmsc.LIMITS_COST.value))
self.check_market_status_limits(
Expand Down
9 changes: 5 additions & 4 deletions tests_additional/real_exchanges/test_polymarket.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
from tests import event_loop

try:
import tentacles.Trading.Exchange.polymarket.ccxt.polymarket_async
from tentacles.Trading.Exchange.polymarket.ccxt.polymarket_async import Polymarket
except ImportError:
# test will be skipped if the tentacle is not installed
pytest.skip(
reason=(
"Polymarket tentacle is not installed, skipping TestPolymarketRealExchangeTester"
)
"Polymarket tentacle is not installed, skipping TestPolymarketRealExchangeTester",
allow_module_level=True
)

# All test coroutines will be treated as marked.
Expand All @@ -44,6 +44,7 @@ class TestPolymarketRealExchangeTester(RealExchangeTester):
SYMBOL_3 = "10pt0-or-above-earthquake-before-2027/USDC:USDC-261231"
TIME_FRAME = TimeFrames.ONE_MINUTE
MARKET_STATUS_TYPE = trading_enums.ExchangeTypes.OPTION.value
USES_TENTACLE = True # set True when an exchange tentacles should be used in this test

async def test_time_frames(self):
time_frames = await self.time_frames()
Expand Down
Loading