From 0d2a27977bf3435f6e93ebea222ca1644d8c3d91 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 8 Jan 2024 10:05:13 +0100 Subject: [PATCH 01/11] [TradingView] fix SIGNAL=CANCEL docs --- .../resources/TradingViewSignalsTradingMode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md index bb8cab7ff..6deaeeb13 100644 --- a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md +++ b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md @@ -44,7 +44,7 @@ Orders can be cancelled using the following format: ``` bash EXCHANGE=binance SYMBOL=ETHBTC -ORDER_TYPE=CANCEL +SIGNAL=CANCEL ``` Additional cancel parameters: From a580429debc358355f8d7fed2a94edf981baffee Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 8 Jan 2024 22:46:37 +0100 Subject: [PATCH 02/11] [Exchange] remove bittrex --- Trading/Exchange/bittrex/__init__.py | 1 - Trading/Exchange/bittrex/bittrex_exchange.py | 54 ------------------- Trading/Exchange/bittrex/metadata.json | 6 --- Trading/Exchange/bittrex/resources/bittrex.md | 1 - .../bittrex_websocket_feed/__init__.py | 1 - .../bittrex_websocket.py | 31 ----------- .../bittrex_websocket_feed/metadata.json | 6 --- .../bittrex_websocket_feed/tests/__init__.py | 15 ------ 8 files changed, 115 deletions(-) delete mode 100644 Trading/Exchange/bittrex/__init__.py delete mode 100644 Trading/Exchange/bittrex/bittrex_exchange.py delete mode 100644 Trading/Exchange/bittrex/metadata.json delete mode 100644 Trading/Exchange/bittrex/resources/bittrex.md delete mode 100644 Trading/Exchange/bittrex_websocket_feed/__init__.py delete mode 100644 Trading/Exchange/bittrex_websocket_feed/bittrex_websocket.py delete mode 100644 Trading/Exchange/bittrex_websocket_feed/metadata.json delete mode 100644 Trading/Exchange/bittrex_websocket_feed/tests/__init__.py diff --git a/Trading/Exchange/bittrex/__init__.py b/Trading/Exchange/bittrex/__init__.py deleted file mode 100644 index e5a0e9714..000000000 --- a/Trading/Exchange/bittrex/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .bittrex_exchange import Bittrex \ No newline at end of file diff --git a/Trading/Exchange/bittrex/bittrex_exchange.py b/Trading/Exchange/bittrex/bittrex_exchange.py deleted file mode 100644 index 180599b6c..000000000 --- a/Trading/Exchange/bittrex/bittrex_exchange.py +++ /dev/null @@ -1,54 +0,0 @@ -# Drakkar-Software OctoBot-Tentacles -# Copyright (c) Drakkar-Software, All rights reserved. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3.0 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. - -import octobot_trading.exchanges as exchanges - - -class Bittrex(exchanges.RestExchange): - DESCRIPTION = "" - - FIX_MARKET_STATUS = True - - SUPPORTED_ORDER_BOOK_LIMITS = [1, 25, 500] - DEFAULT_ORDER_BOOK_LIMIT = 25 - - @classmethod - def get_name(cls): - return 'bittrex' - - async def get_order_book(self, symbol, limit=DEFAULT_ORDER_BOOK_LIMIT, **kwargs): - if limit is None or limit not in self.SUPPORTED_ORDER_BOOK_LIMITS: - self.logger.debug(f"Trying to get_order_book with limit not {self.SUPPORTED_ORDER_BOOK_LIMITS} : ({limit})") - limit = self.DEFAULT_ORDER_BOOK_LIMIT - return await super().get_recent_trades(symbol=symbol, limit=limit, **kwargs) - - async def get_symbol_prices(self, symbol, time_frame, limit: int = None, **kwargs: dict): - # ohlcv limit is not working as expected, limit is doing [:-limit] but we want [-limit:] - candles = await super().get_symbol_prices(symbol=symbol, time_frame=time_frame, limit=limit, **kwargs) - if limit: - return candles[-limit:] - return candles - - async def get_price_ticker(self, symbol: str, **kwargs: dict): - """ - Multiple calls are required to get all ticker data - https://github.com/ccxt/ccxt/issues/7893 - Default ccxt call is using publicGetMarketsMarketSymbolTicker - But the mandatory data is available by calling publicGetMarketsMarketSymbolSummary - """ - if "method" not in kwargs: - kwargs["method"] = "publicGetMarketsMarketSymbolSummary" - return await super().get_price_ticker(symbol=symbol, **kwargs) diff --git a/Trading/Exchange/bittrex/metadata.json b/Trading/Exchange/bittrex/metadata.json deleted file mode 100644 index 0633b93cb..000000000 --- a/Trading/Exchange/bittrex/metadata.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "1.2.0", - "origin_package": "OctoBot-Default-Tentacles", - "tentacles": ["Bittrex"], - "tentacles-requirements": [] -} \ No newline at end of file diff --git a/Trading/Exchange/bittrex/resources/bittrex.md b/Trading/Exchange/bittrex/resources/bittrex.md deleted file mode 100644 index 63c32bf6a..000000000 --- a/Trading/Exchange/bittrex/resources/bittrex.md +++ /dev/null @@ -1 +0,0 @@ -Bittrex is a basic RestExchange adaptation for Bittrex exchange. diff --git a/Trading/Exchange/bittrex_websocket_feed/__init__.py b/Trading/Exchange/bittrex_websocket_feed/__init__.py deleted file mode 100644 index 8e1e436fd..000000000 --- a/Trading/Exchange/bittrex_websocket_feed/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .bittrex_websocket import BittrexCCXTWebsocketConnector diff --git a/Trading/Exchange/bittrex_websocket_feed/bittrex_websocket.py b/Trading/Exchange/bittrex_websocket_feed/bittrex_websocket.py deleted file mode 100644 index ea948bb1b..000000000 --- a/Trading/Exchange/bittrex_websocket_feed/bittrex_websocket.py +++ /dev/null @@ -1,31 +0,0 @@ -# Drakkar-Software OctoBot-Tentacles -# Copyright (c) Drakkar-Software, All rights reserved. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3.0 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. -import octobot_trading.exchanges as exchanges -from octobot_trading.enums import WebsocketFeeds as Feeds -import tentacles.Trading.Exchange.bittrex.bittrex_exchange as bittrex_exchange - - -class BittrexCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector): - EXCHANGE_FEEDS = { - Feeds.TRADES: True, - Feeds.KLINE: True, - Feeds.TICKER: True, - Feeds.CANDLE: True, - } - - @classmethod - def get_name(cls): - return bittrex_exchange.Bittrex.get_name() diff --git a/Trading/Exchange/bittrex_websocket_feed/metadata.json b/Trading/Exchange/bittrex_websocket_feed/metadata.json deleted file mode 100644 index 190eac979..000000000 --- a/Trading/Exchange/bittrex_websocket_feed/metadata.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "1.2.0", - "origin_package": "OctoBot-Default-Tentacles", - "tentacles": ["BittrexCCXTWebsocketConnector"], - "tentacles-requirements": [] -} \ No newline at end of file diff --git a/Trading/Exchange/bittrex_websocket_feed/tests/__init__.py b/Trading/Exchange/bittrex_websocket_feed/tests/__init__.py deleted file mode 100644 index 974dd1623..000000000 --- a/Trading/Exchange/bittrex_websocket_feed/tests/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Drakkar-Software OctoBot-Tentacles -# Copyright (c) Drakkar-Software, All rights reserved. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3.0 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. From 5ff164f984c09127266cc42cf1df717abf7686f3 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 8 Jan 2024 22:47:42 +0100 Subject: [PATCH 03/11] [Exchanges] update for ccxt 4.2.10 --- Trading/Exchange/bitfinex/bitfinex_exchange.py | 13 +++++++++++++ Trading/Exchange/cryptocom/cryptocom_exchange.py | 2 -- Trading/Exchange/hollaex/hollaex_exchange.py | 15 +++++++++++++++ Trading/Exchange/kraken/kraken_exchange.py | 13 +++++++++++++ Trading/Exchange/kucoin/kucoin_exchange.py | 12 ++++++++++++ Trading/Exchange/phemex/phemex_exchange.py | 12 +++++++++++- 6 files changed, 64 insertions(+), 3 deletions(-) diff --git a/Trading/Exchange/bitfinex/bitfinex_exchange.py b/Trading/Exchange/bitfinex/bitfinex_exchange.py index 57ce0dcd8..adb72a98b 100644 --- a/Trading/Exchange/bitfinex/bitfinex_exchange.py +++ b/Trading/Exchange/bitfinex/bitfinex_exchange.py @@ -18,6 +18,7 @@ import octobot_commons.enums import octobot_commons.constants import octobot_trading.exchanges as exchanges +import octobot_trading.enums as trading_enums class Bitfinex(exchanges.RestExchange): @@ -32,6 +33,9 @@ class Bitfinex(exchanges.RestExchange): def get_name(cls): return 'bitfinex2' + def get_adapter_class(self): + return BitfinexCCXTAdapter + async def get_symbol_prices(self, symbol, time_frame, limit: int = 500, **kwargs: dict): if "since" not in kwargs: # prevent bitfinex from getting candles from 2014 @@ -50,3 +54,12 @@ async def get_order_book(self, symbol, limit=DEFAULT_ORDER_BOOK_LIMIT, **kwargs) self.logger.debug(f"Trying to get_order_book with limit not {self.SUPPORTED_ORDER_BOOK_LIMITS} : ({limit})") limit = self.DEFAULT_ORDER_BOOK_LIMIT return await super().get_recent_trades(symbol=symbol, limit=limit, **kwargs) + + +class BitfinexCCXTAdapter(exchanges.CCXTAdapter): + + def fix_ticker(self, raw, **kwargs): + fixed = super().fix_ticker(raw, **kwargs) + fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \ + fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds() + return fixed diff --git a/Trading/Exchange/cryptocom/cryptocom_exchange.py b/Trading/Exchange/cryptocom/cryptocom_exchange.py index 4a834d20b..d42c535e5 100644 --- a/Trading/Exchange/cryptocom/cryptocom_exchange.py +++ b/Trading/Exchange/cryptocom/cryptocom_exchange.py @@ -21,8 +21,6 @@ class CryptoCom(exchanges.RestExchange): FIX_MARKET_STATUS = True - REQUIRE_CLOSED_ORDERS_FROM_RECENT_TRADES = True # set True when get_closed_orders is not supported - @classmethod def get_name(cls): return 'cryptocom' diff --git a/Trading/Exchange/hollaex/hollaex_exchange.py b/Trading/Exchange/hollaex/hollaex_exchange.py index a46bb2b48..84aec0842 100644 --- a/Trading/Exchange/hollaex/hollaex_exchange.py +++ b/Trading/Exchange/hollaex/hollaex_exchange.py @@ -16,6 +16,7 @@ import ccxt import octobot_commons.enums as commons_enums +import octobot_trading.enums as trading_enums import octobot_trading.exchanges as exchanges import octobot_trading.exchanges.connectors.ccxt.enums as ccxt_enums @@ -39,6 +40,9 @@ def __init__(self, config, exchange_manager): self.HAS_WEBSOCKETS_KEY, not self.exchange_manager.rest_only ) + def get_adapter_class(self): + return HollaexCCXTAdapter + @classmethod def init_user_inputs_from_class(cls, inputs: dict) -> None: """ @@ -91,3 +95,14 @@ async def get_closed_orders(self, symbol: str = None, since: int = None, symbol=symbol, since=since, limit=limit, **kwargs ) ) + + +class HollaexCCXTAdapter(exchanges.CCXTAdapter): + + def fix_order(self, raw, symbol=None, **kwargs): + raw_order_info = raw[ccxt_enums.ExchangePositionCCXTColumns.INFO.value] + # average is not supported by ccxt + fixed = super().fix_order(raw, **kwargs) + if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] and "average" in raw_order_info: + fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = raw_order_info.get("average", 0) + return fixed \ No newline at end of file diff --git a/Trading/Exchange/kraken/kraken_exchange.py b/Trading/Exchange/kraken/kraken_exchange.py index 567bf4267..aa88dee04 100644 --- a/Trading/Exchange/kraken/kraken_exchange.py +++ b/Trading/Exchange/kraken/kraken_exchange.py @@ -15,6 +15,7 @@ # License along with this library. import octobot_trading.exchanges as exchanges import octobot_trading.errors +import octobot_trading.enums as trading_enums class Kraken(exchanges.RestExchange): @@ -33,6 +34,9 @@ def __init__(self, config, exchange_manager): def get_name(cls): return 'kraken' + def get_adapter_class(self): + return KrakenCCXTAdapter + async def get_recent_trades(self, symbol, limit=RECENT_TRADE_FIXED_LIMIT, **kwargs): if limit is not None and limit != self.RECENT_TRADE_FIXED_LIMIT: self.logger.debug(f"Trying to get_recent_trades with limit != {self.RECENT_TRADE_FIXED_LIMIT} : ({limit})") @@ -52,3 +56,12 @@ async def get_symbol_prices(self, symbol, time_frame, limit: int = None, **kwarg if limit: return candles[-limit:] return candles + + +class KrakenCCXTAdapter(exchanges.CCXTAdapter): + + def fix_ticker(self, raw, **kwargs): + fixed = super().fix_ticker(raw, **kwargs) + fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \ + fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds() + return fixed diff --git a/Trading/Exchange/kucoin/kucoin_exchange.py b/Trading/Exchange/kucoin/kucoin_exchange.py index 7f7af5b3e..a9b8886d6 100644 --- a/Trading/Exchange/kucoin/kucoin_exchange.py +++ b/Trading/Exchange/kucoin/kucoin_exchange.py @@ -209,6 +209,11 @@ async def get_order_book(self, symbol, limit=20, **kwargs): # override default limit to be kucoin complient return super().get_order_book(symbol, limit=limit, **kwargs) + @_kucoin_retrier + async def get_order_book(self, symbol, limit=20, **kwargs): + # override default limit to be kucoin complient + return super().get_order_book(symbol, limit=limit, **kwargs) + def should_log_on_ddos_exception(self, exception) -> bool: """ Override when necessary @@ -386,6 +391,7 @@ class KucoinCCXTAdapter(exchanges.CCXTAdapter): def fix_order(self, raw, symbol=None, **kwargs): raw_order_info = raw[ccxt_enums.ExchangePositionCCXTColumns.INFO.value] fixed = super().fix_order(raw, **kwargs) + self._ensure_fees(fixed) if self.connector.exchange_manager.is_future and \ fixed[trading_enums.ExchangeConstantsOrderColumns.COST.value] is not None: fixed[trading_enums.ExchangeConstantsOrderColumns.COST.value] = \ @@ -394,6 +400,12 @@ def fix_order(self, raw, symbol=None, **kwargs): self._adapt_order_type(fixed) return fixed + def fix_trades(self, raw, **kwargs): + fixed = super().fix_trades(raw, **kwargs) + for trade in fixed: + self._ensure_fees(trade) + return fixed + def _adapt_order_type(self, fixed): order_info = fixed[trading_enums.ExchangeConstantsOrderColumns.INFO.value] if trigger_direction := order_info.get("stop", None): diff --git a/Trading/Exchange/phemex/phemex_exchange.py b/Trading/Exchange/phemex/phemex_exchange.py index 6adfbcaba..088065213 100644 --- a/Trading/Exchange/phemex/phemex_exchange.py +++ b/Trading/Exchange/phemex/phemex_exchange.py @@ -16,6 +16,7 @@ import asyncio import decimal import typing +import ccxt import octobot_commons.enums as commons_enums import octobot_commons.constants as commons_constants @@ -77,6 +78,15 @@ def _get_ohlcv_params(self, time_frame, limit, **kwargs): }) return kwargs + async def cancel_order( + self, exchange_order_id: str, symbol: str, order_type: trading_enums.TraderOrderType, **kwargs: dict + ) -> trading_enums.OrderStatus: + order_status = await super().cancel_order(exchange_order_id, symbol, order_type, **kwargs) + if order_status == trading_enums.OrderStatus.PENDING_CANCEL: + # cancelled orders can't be fetched, consider as cancelled + order_status = trading_enums.OrderStatus.CANCELED + return order_status + async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict: if order := await self.connector.get_order(symbol=symbol, exchange_order_id=exchange_order_id, **kwargs): return order @@ -93,7 +103,7 @@ async def _get_order_from_trades(self, symbol, exchange_order_id, order_to_updat await asyncio.sleep(3) else: return order - raise KeyError("Order id not found in trades. Impossible to build order from trades history") + raise ccxt.OrderNotFound("Order id not found in trades. Impossible to build order from trades history") class PhemexCCXTAdapter(exchanges.CCXTAdapter): From e23de5638c6a05cc0df5c6421928610da2d841ea Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 00:17:30 +0100 Subject: [PATCH 04/11] [Exchnages] replace Huobi by HTX --- .../web_interface/models/configuration.py | 1 + Trading/Exchange/htx/__init__.py | 1 + Trading/Exchange/htx/htx_exchange.py | 76 +++++++++++++++++++ Trading/Exchange/htx/metadata.json | 6 ++ Trading/Exchange/htx/resources/htx.md | 1 + .../Exchange/htx_websocket_feed/__init__.py | 1 + .../htx_websocket.py} | 19 +++++ .../Exchange/htx_websocket_feed/metadata.json | 6 ++ Trading/Exchange/huobi/huobi_exchange.py | 59 +------------- .../huobi_websocket_feed/huobi_websocket.py | 15 +--- 10 files changed, 117 insertions(+), 68 deletions(-) create mode 100644 Trading/Exchange/htx/__init__.py create mode 100644 Trading/Exchange/htx/htx_exchange.py create mode 100644 Trading/Exchange/htx/metadata.json create mode 100644 Trading/Exchange/htx/resources/htx.md create mode 100644 Trading/Exchange/htx_websocket_feed/__init__.py rename Trading/Exchange/{bittrex/tests/__init__.py => htx_websocket_feed/htx_websocket.py} (56%) create mode 100644 Trading/Exchange/htx_websocket_feed/metadata.json diff --git a/Services/Interfaces/web_interface/models/configuration.py b/Services/Interfaces/web_interface/models/configuration.py index 61bad67f6..f55c7736b 100644 --- a/Services/Interfaces/web_interface/models/configuration.py +++ b/Services/Interfaces/web_interface/models/configuration.py @@ -110,6 +110,7 @@ for result, merged in ( (ccxt.async_support.kucoin, (ccxt.async_support.kucoinfutures, )), (ccxt.async_support.binance, (ccxt.async_support.binanceusdm, ccxt.async_support.binancecoinm)), + (ccxt.async_support.htx, (ccxt.async_support.huobi, )), ) } REMOVED_CCXT_EXCHANGES = set().union(*(set(v) for v in MERGED_CCXT_EXCHANGES.values())) diff --git a/Trading/Exchange/htx/__init__.py b/Trading/Exchange/htx/__init__.py new file mode 100644 index 000000000..b2e66c373 --- /dev/null +++ b/Trading/Exchange/htx/__init__.py @@ -0,0 +1 @@ +from .htx_exchange import Htx \ No newline at end of file diff --git a/Trading/Exchange/htx/htx_exchange.py b/Trading/Exchange/htx/htx_exchange.py new file mode 100644 index 000000000..375d628e2 --- /dev/null +++ b/Trading/Exchange/htx/htx_exchange.py @@ -0,0 +1,76 @@ +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. +import decimal +import typing + +import octobot_trading.exchanges as exchanges +import octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants +import octobot_trading.enums as trading_enums +import octobot_trading.errors + + +class Htx(exchanges.RestExchange): + FIX_MARKET_STATUS = True + REMOVE_MARKET_STATUS_PRICE_LIMITS = True + + @classmethod + def get_name(cls): + return 'htx' + + def get_adapter_class(self): + return HtxCCXTAdapter + + def get_additional_connector_config(self): + # tell ccxt to use amount as provided and not to compute it by multiplying it by price which is done here + # (price should not be sent to market orders). Only used for buy market orders + return { + ccxt_constants.CCXT_OPTIONS: { + "createMarketBuyOrderRequiresPrice": False # disable quote conversion + } + } + + async def create_order(self, order_type: trading_enums.TraderOrderType, symbol: str, quantity: decimal.Decimal, + price: decimal.Decimal = None, stop_price: decimal.Decimal = None, + side: trading_enums.TradeOrderSide = None, current_price: decimal.Decimal = None, + reduce_only: bool = False, params: dict = None) -> typing.Optional[dict]: + if order_type is trading_enums.TraderOrderType.BUY_MARKET: + # on HTX, market orders are in quote currency (YYY in XYZ/YYY) + used_price = price or current_price + if not used_price: + raise octobot_trading.errors.NotSupported(f"{self.get_name()} requires a price parameter to create " + f"market orders as quantity is in quote currency") + quantity = quantity * used_price + return await super().create_order(order_type, symbol, quantity, + price=price, stop_price=stop_price, + side=side, current_price=current_price, + reduce_only=reduce_only, params=params) + + +class HtxCCXTAdapter(exchanges.CCXTAdapter): + + def fix_order(self, raw, **kwargs): + fixed = super().fix_order(raw, **kwargs) + try: + if fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] \ + == trading_enums.TradeOrderType.MARKET.value and \ + fixed[trading_enums.ExchangeConstantsOrderColumns.SIDE.value] \ + == trading_enums.TradeOrderSide.BUY.value: + # convert amount to have the same units as evert other exchange: use FILLED for accuracy + fixed[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] = \ + fixed[trading_enums.ExchangeConstantsOrderColumns.FILLED.value] + except KeyError: + pass + return fixed diff --git a/Trading/Exchange/htx/metadata.json b/Trading/Exchange/htx/metadata.json new file mode 100644 index 000000000..b9d703501 --- /dev/null +++ b/Trading/Exchange/htx/metadata.json @@ -0,0 +1,6 @@ +{ + "version": "1.2.0", + "origin_package": "OctoBot-Default-Tentacles", + "tentacles": ["Htx"], + "tentacles-requirements": [] +} \ No newline at end of file diff --git a/Trading/Exchange/htx/resources/htx.md b/Trading/Exchange/htx/resources/htx.md new file mode 100644 index 000000000..7b9717fdb --- /dev/null +++ b/Trading/Exchange/htx/resources/htx.md @@ -0,0 +1 @@ +HTX is a basic RestExchange adaptation for HTX exchange. diff --git a/Trading/Exchange/htx_websocket_feed/__init__.py b/Trading/Exchange/htx_websocket_feed/__init__.py new file mode 100644 index 000000000..51a4731b5 --- /dev/null +++ b/Trading/Exchange/htx_websocket_feed/__init__.py @@ -0,0 +1 @@ +from .htx_websocket import HtxCCXTWebsocketConnector diff --git a/Trading/Exchange/bittrex/tests/__init__.py b/Trading/Exchange/htx_websocket_feed/htx_websocket.py similarity index 56% rename from Trading/Exchange/bittrex/tests/__init__.py rename to Trading/Exchange/htx_websocket_feed/htx_websocket.py index 974dd1623..1ca1dfc8a 100644 --- a/Trading/Exchange/bittrex/tests/__init__.py +++ b/Trading/Exchange/htx_websocket_feed/htx_websocket.py @@ -13,3 +13,22 @@ # # You should have received a copy of the GNU Lesser General Public # License along with this library. +import octobot_trading.exchanges as exchanges +from octobot_trading.enums import WebsocketFeeds as Feeds +import tentacles.Trading.Exchange.htx.htx_exchange as htx_exchange + + +class HtxCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector): + EXCHANGE_FEEDS = { + Feeds.TRADES: True, + Feeds.KLINE: True, + Feeds.TICKER: True, + Feeds.CANDLE: True, + } + + @classmethod + def get_name(cls): + return htx_exchange.Htx.get_name() + + def get_adapter_class(self, adapter_class): + return htx_exchange.HtxCCXTAdapter diff --git a/Trading/Exchange/htx_websocket_feed/metadata.json b/Trading/Exchange/htx_websocket_feed/metadata.json new file mode 100644 index 000000000..44d14c63f --- /dev/null +++ b/Trading/Exchange/htx_websocket_feed/metadata.json @@ -0,0 +1,6 @@ +{ + "version": "1.2.0", + "origin_package": "OctoBot-Default-Tentacles", + "tentacles": ["HtxCCXTWebsocketConnector"], + "tentacles-requirements": [] +} \ No newline at end of file diff --git a/Trading/Exchange/huobi/huobi_exchange.py b/Trading/Exchange/huobi/huobi_exchange.py index 36b673dd7..327952709 100644 --- a/Trading/Exchange/huobi/huobi_exchange.py +++ b/Trading/Exchange/huobi/huobi_exchange.py @@ -13,64 +13,11 @@ # # You should have received a copy of the GNU Lesser General Public # License along with this library. -import decimal -import typing +import tentacles.Trading.Exchange.htx.htx_exchange as htx_exchange -import octobot_trading.exchanges as exchanges -import octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants -import octobot_trading.enums as trading_enums -import octobot_trading.errors - - -class Huobi(exchanges.RestExchange): - FIX_MARKET_STATUS = True - REMOVE_MARKET_STATUS_PRICE_LIMITS = True +class Huobi(htx_exchange.Htx): + # kept for legacy support (users using huobi instead of HTX) @classmethod def get_name(cls): return 'huobi' - - def get_adapter_class(self): - return HuobiCCXTAdapter - - def get_additional_connector_config(self): - # tell ccxt to use amount as provided and not to compute it by multiplying it by price which is done here - # (price should not be sent to market orders). Only used for buy market orders - return { - ccxt_constants.CCXT_OPTIONS: { - "createMarketBuyOrderRequiresPrice": False # disable quote conversion - } - } - - async def create_order(self, order_type: trading_enums.TraderOrderType, symbol: str, quantity: decimal.Decimal, - price: decimal.Decimal = None, stop_price: decimal.Decimal = None, - side: trading_enums.TradeOrderSide = None, current_price: decimal.Decimal = None, - reduce_only: bool = False, params: dict = None) -> typing.Optional[dict]: - if order_type is trading_enums.TraderOrderType.BUY_MARKET: - # on Huobi, market orders are in quote currency (YYY in XYZ/YYY) - used_price = price or current_price - if not used_price: - raise octobot_trading.errors.NotSupported(f"{self.get_name()} requires a price parameter to create " - f"market orders as quantity is in quote currency") - quantity = quantity * used_price - return await super().create_order(order_type, symbol, quantity, - price=price, stop_price=stop_price, - side=side, current_price=current_price, - reduce_only=reduce_only, params=params) - - -class HuobiCCXTAdapter(exchanges.CCXTAdapter): - - def fix_order(self, raw, **kwargs): - fixed = super().fix_order(raw, **kwargs) - try: - if fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] \ - == trading_enums.TradeOrderType.MARKET.value and \ - fixed[trading_enums.ExchangeConstantsOrderColumns.SIDE.value] \ - == trading_enums.TradeOrderSide.BUY.value: - # convert amount to have the same units as evert other exchange: use FILLED for accuracy - fixed[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] = \ - fixed[trading_enums.ExchangeConstantsOrderColumns.FILLED.value] - except KeyError: - pass - return fixed diff --git a/Trading/Exchange/huobi_websocket_feed/huobi_websocket.py b/Trading/Exchange/huobi_websocket_feed/huobi_websocket.py index 71f2daf99..7b73a0899 100644 --- a/Trading/Exchange/huobi_websocket_feed/huobi_websocket.py +++ b/Trading/Exchange/huobi_websocket_feed/huobi_websocket.py @@ -13,22 +13,13 @@ # # You should have received a copy of the GNU Lesser General Public # License along with this library. -import octobot_trading.exchanges as exchanges -from octobot_trading.enums import WebsocketFeeds as Feeds import tentacles.Trading.Exchange.huobi.huobi_exchange as huobi_exchange +import tentacles.Trading.Exchange.htx_websocket_feed.htx_websocket as htx_websocket -class HuobiCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector): - EXCHANGE_FEEDS = { - Feeds.TRADES: True, - Feeds.KLINE: True, - Feeds.TICKER: True, - Feeds.CANDLE: True, - } - +class HuobiCCXTWebsocketConnector(htx_websocket.HtxCCXTWebsocketConnector): + # kept for legacy support (users using huobi instead of HTX) @classmethod def get_name(cls): return huobi_exchange.Huobi.get_name() - def get_adapter_class(self, adapter_class): - return huobi_exchange.HuobiCCXTAdapter From 10328d39c2d13e47a9cbea0a2e6eaa2a275f7d5b Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 00:17:52 +0100 Subject: [PATCH 05/11] [MEXC] fix order fetch --- Trading/Exchange/mexc/mexc_exchange.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Trading/Exchange/mexc/mexc_exchange.py b/Trading/Exchange/mexc/mexc_exchange.py index 0b7712cff..f931fc1a5 100644 --- a/Trading/Exchange/mexc/mexc_exchange.py +++ b/Trading/Exchange/mexc/mexc_exchange.py @@ -95,6 +95,41 @@ async def _mexc_handled_symbols_filter(self, symbol): ) raise err + async def get_open_orders(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list: + return self._filter_orders( + await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs), + True + ) + + async def get_closed_orders(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list: + return self._filter_orders( + await super().get_closed_orders(symbol=symbol, since=since, limit=limit, **kwargs), + False + ) + + async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict: + try: + return await super().get_order( + exchange_order_id, symbol=symbol, **kwargs + ) + except octobot_trading.errors.FailedRequest as err: + if "Order does not exist" in str(err): + return None + raise + + def _filter_orders(self, orders: list, open_only: bool) -> list: + return [ + order + for order in orders + if ( + open_only and order[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] + == trading_enums.OrderStatus.OPEN.value + ) or ( + not open_only and order[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] + != trading_enums.OrderStatus.OPEN.value + ) + ] + class APIHandledSymbols: """ From 185df8ad0bc756609a94797537c110d7ccf6166b Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 12:27:53 +0100 Subject: [PATCH 06/11] [GPT] update openai api --- Services/Services_bases/gpt_service/gpt.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Services/Services_bases/gpt_service/gpt.py b/Services/Services_bases/gpt_service/gpt.py index f71a9a4b6..16060231d 100644 --- a/Services/Services_bases/gpt_service/gpt.py +++ b/Services/Services_bases/gpt_service/gpt.py @@ -24,7 +24,6 @@ import octobot_services.errors as errors import octobot_commons.enums as commons_enums -import octobot_commons.constants as commons_constants import octobot_commons.time_frame_manager as time_frame_manager import octobot_commons.authentication as authentication import octobot_commons.tree as tree @@ -92,6 +91,9 @@ async def get_chat_completion( return await self._fetch_signal_from_stored_signals(exchange, symbol, time_frame, version, candle_open_time) return await self._get_signal_from_gpt(messages, model, max_tokens, n, stop, temperature) + def _get_client(self) -> openai.AsyncOpenAI: + return openai.AsyncOpenAI(api_key=self._get_api_key()) + async def _get_signal_from_gpt( self, messages, @@ -104,8 +106,7 @@ async def _get_signal_from_gpt( self._ensure_rate_limit() try: model = model or self.model - completions = await openai.ChatCompletion.acreate( - api_key=self._get_api_key(), + completions = await self._get_client().chat.completions.create( model=model, max_tokens=max_tokens, n=n, @@ -113,9 +114,9 @@ async def _get_signal_from_gpt( temperature=temperature, messages=messages ) - self._update_token_usage(completions['usage']['total_tokens']) - return completions["choices"][0]["message"]["content"] - except openai.error.InvalidRequestError as err: + self._update_token_usage(completions.usage.total_tokens) + return completions.choices[0].message.content + except openai.BadRequestError as err: raise errors.InvalidRequestError( f"Error when running request with model {model} (invalid request): {err}" ) from err @@ -303,12 +304,12 @@ async def prepare(self) -> None: if self.use_stored_signals_only(): self.logger.info(f"Skipping GPT - OpenAI models fetch as self.use_stored_signals_only() is True") return - fetched_models = await openai.Model.alist(api_key=self._get_api_key()) - self.models = [d["id"] for d in fetched_models["data"]] + fetched_models = await self._get_client().models.list() + self.models = [d.id for d in fetched_models.data] if self.model not in self.models: self.logger.warning(f"Warning: selected '{self.model}' model is not in GPT available models. " f"Available models are: {self.models}") - except openai.error.AuthenticationError as err: + except openai.AuthenticationError as err: self.logger.error(f"Error when checking api key: {err}") except Exception as err: self.logger.error(f"Unexpected error when checking api key: {err}") From 80dd2f83cd0e440ceb2100187f12cff82f834e29 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 16:10:48 +0100 Subject: [PATCH 07/11] [Docs] update guides link --- .../resources/TelegramChannelSignalEvaluator.md | 2 +- .../signal_evaluator/resources/TelegramSignalEvaluator.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Evaluator/Social/signal_evaluator/resources/TelegramChannelSignalEvaluator.md b/Evaluator/Social/signal_evaluator/resources/TelegramChannelSignalEvaluator.md index d8160334b..9deab3dc5 100644 --- a/Evaluator/Social/signal_evaluator/resources/TelegramChannelSignalEvaluator.md +++ b/Evaluator/Social/signal_evaluator/resources/TelegramChannelSignalEvaluator.md @@ -4,4 +4,4 @@ Triggers on a Telegram signal from any channel your personal account joined. Signal parsing is configurable according to the name of the channel. -See [OctoBot docs about Telegram API service](https://www.octobot.cloud/guides/octobot-interfaces/telegram/telegram-api?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=telegramChannelSignalEvaluator) for more information. +See [OctoBot docs about Telegram API service](https://www.octobot.cloud/en/guides/octobot-interfaces/telegram/telegram-api?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=telegramChannelSignalEvaluator) for more information. diff --git a/Evaluator/Social/signal_evaluator/resources/TelegramSignalEvaluator.md b/Evaluator/Social/signal_evaluator/resources/TelegramSignalEvaluator.md index daa2c17af..a1f9b3b49 100644 --- a/Evaluator/Social/signal_evaluator/resources/TelegramSignalEvaluator.md +++ b/Evaluator/Social/signal_evaluator/resources/TelegramSignalEvaluator.md @@ -11,4 +11,4 @@ Remember that OctoBot can only see messages from a chat/group where its Telegram bot (in OctoBot configuration) has been invited. Keep also in mind that you need to disable the privacy mode of your Telegram bot to allow it to see group messages. -See [OctoBot docs about Telegram interface](https://www.octobot.cloud/guides/octobot-interfaces/telegram?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=telegramSignalEvaluator) for more information. +See [OctoBot docs about Telegram interface](https://www.octobot.cloud/en/guides/octobot-interfaces/telegram?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=telegramSignalEvaluator) for more information. From ca514383782d1f11acd8214add41b9940b11c4b4 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 23:01:16 +0100 Subject: [PATCH 08/11] [GPT] call patch_openai_proxies --- Services/Services_bases/gpt_service/gpt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Services/Services_bases/gpt_service/gpt.py b/Services/Services_bases/gpt_service/gpt.py index 16060231d..e30d3f8d0 100644 --- a/Services/Services_bases/gpt_service/gpt.py +++ b/Services/Services_bases/gpt_service/gpt.py @@ -22,6 +22,7 @@ import octobot_services.constants as services_constants import octobot_services.services as services import octobot_services.errors as errors +import octobot_services.util import octobot_commons.enums as commons_enums import octobot_commons.time_frame_manager as time_frame_manager @@ -32,6 +33,9 @@ import octobot.community as community +octobot_services.util.patch_openai_proxies() + + class GPTService(services.AbstractService): BACKTESTING_ENABLED = True DEFAULT_MODEL = "gpt-3.5-turbo" From c38b2301e1e6bfe9fbe50c1717f8a30a4bcc8ff6 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 23:04:51 +0100 Subject: [PATCH 09/11] [WebInterface] update logo --- .../advanced_templates/advanced_layout.html | 2 +- .../web_interface/static/favicon.ico | Bin 15406 -> 0 bytes .../web_interface/static/favicon.png | Bin 0 -> 1530 bytes .../static/img/community/octobot-cloud.png | Bin 9471 -> 20070 bytes .../web_interface/templates/layout.html | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 Services/Interfaces/web_interface/static/favicon.ico create mode 100644 Services/Interfaces/web_interface/static/favicon.png diff --git a/Services/Interfaces/web_interface/advanced_templates/advanced_layout.html b/Services/Interfaces/web_interface/advanced_templates/advanced_layout.html index 5482c06f5..e9d156e7c 100644 --- a/Services/Interfaces/web_interface/advanced_templates/advanced_layout.html +++ b/Services/Interfaces/web_interface/advanced_templates/advanced_layout.html @@ -12,7 +12,7 @@ - + {% block additional_meta %} {% endblock additional_meta %} diff --git a/Services/Interfaces/web_interface/static/favicon.ico b/Services/Interfaces/web_interface/static/favicon.ico deleted file mode 100644 index a029fedd998a78f2dccc2652e37864d35b16754f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 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 Z?s-JhE_o?6o}#6Y|B~jH7FdD>{ue7CbOZnZ diff --git a/Services/Interfaces/web_interface/static/favicon.png b/Services/Interfaces/web_interface/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8d1ade398e1880a6484744affe8cfcc14f9259d5 GIT binary patch literal 1530 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGZhyVZuhylMZe;xn;1&>KYK~z{r?Urk7 z6jc;}&%HDIXm>liv$V8AZ3&iA3Physwgdzvyn@sK5ds($BZwM>L?K51U;@G)C`i;q z6C@Z3LamSfA)yi}qO~PO2n7q(LT!PD(tUK>-RDMG#Xkk)T z9Q6u`Th@WDPxbrjnv8j;yZ9Xx@(wNu2n9m;!i&pr+_-E!dh{6kt?3F=)6-cb67jjy zQVTjePj@8_g*md~iQZ<22kBDi6B5yT=2J;b8!5?e6|L#PsMEL#pSPI;nea= zbW%a$9Kxk~x2x&1QB_N4nW6y(C1G1zZ8Y&C7Gh*|3=q|fj5*FwZ{ODG(;0?p4=A;<#UP0>5^c#+d$JaI8jrJ&COSla|YGbKVVBs8#o*(cQ(^H;F382 zAeSYoLjXDwg8xDxl?u7;mYO~bol4Q3f+^*%kI!2e*}n7BLLE8Shb?2hZVR0@X}J0_FqBh(^L%v@a^# z$=ynt0~zNS>yI&ifO3XbP>BnQIq9|0X%q_MTWH-NwB=L+Oj?hq8&%LlOS8#8R04koZyRJTEhZOzLv$1>-dc z+O{5F!%lNy62o@FVPVwJfYA%4$l`JpkEbT{VOm-VRYT&XbA$=Dfx>@V=6s$c#agLP zEgMd`!F_wc?eRVj%yLkm9;BG3P<$`iB>U{HuG)Y6M>HJwZGeHQ7EeKDn~qFe4|dm` gFvTN`@Bje(1x>ti=prn^yZ`_I07*qoM6N<$f`C8VbpQYW literal 0 HcmV?d00001 diff --git a/Services/Interfaces/web_interface/static/img/community/octobot-cloud.png b/Services/Interfaces/web_interface/static/img/community/octobot-cloud.png index 645fddb4b3605d35c6db6343275cc9a38ac13e49..c3c55e1aab9403f9e0184ba289d03add505d89e5 100644 GIT binary patch literal 20070 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%0Aoz8r+T=`Alr`pnT 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 L)01|K8YcJ;RI;?F literal 9471 zcmZ{K3p|ut_y2xo#>`_39=F`WpbMQY3WZ!AL+Esbl29=ugcBv@7LWTSN_8qpjC7GG zB_$=}u9Jq!wa`$Z+@_R7>bFPd{k`x1^ZUPjJ`J<>cYoJfd+)XO-fK&--)t>TSE2&| z#5dYlZUKPI1c3Aer9uf3KX^g-x!1zNexrp2Gw48|_uha#0BD^!a>B%>7GGHJzV))k zHro2+gH879T`;-#1>3Y(3HHC;`M^Ff7r#V&W9rd-zH5+R_pJ~7fuiei*O_P(VPIsa zU15{*a}#}qr?J6!UXpy!^ z%f}E&HTMZm<0rE{xx1O}I*NRm6rXeZoa>b9bkCpRh2A{aZVtf+Q{s_-8uv~to!uh?Y(^|)Yy#qFG?ON_l71=`%>s^XFTxN5=CsfDYz1lSR58w0N@Aur6 zzJ%zLfAvFwOnQJ{_jvWX@oujX^1X$7y5nPK7XJOzXDjQY3lWP~-1-pfty9Dk753YW zXT`fueAi3xlnOP=5BwXcy@&tIdbJ}$?W!c6Sn+!=pe5mVEW1ZkC&ov{$6o7>{fGkm zxdSgmNJA=cTBx3ztS!MJpa-m}UDxZNL^R08B^b_Nr|>_*(mEdsg|vekZ8y*c84D$} zmN*$WSOUNV8!gQpLtl>dZakpeDJ^2piIsAVTX3!2_k?Sl)hDj``70YQUhDJ<=1oU$ zinR6aTSipugiH(WyOM~RN71w2mcDsC+c}{&BTE1hQKJ4AU=jdbXe+uHkWfHM(jh@P z^B}qipiC&QL4~kZ^|X1I3aTkYvHn4YGLwZU7VQ^`AHyUG$yDc2L@WF%@&D2I-%$Un5mkrQ{})sZ4cY}s1;~FzWmJ2J z0zhmtt_O*qrpO;A7f>)9^pi0bKq#0gP^aK9#wBqSxhq(F5b*b^;!quX8i1NlFJ&=P z;zgva*grjpph$`s0L~4k=zNd9D3JT{|Bwwg9>$@%J;oQn_`n!VpcQCN91kUv09gL! zF(v5KJvDCWLlLMMp9b+5TUo6dHl6usNt*r(Hfi<6f}0N!L@3fDO=m!nFBHL1jD&+% z^4D=TY|OY})73He_8N|G%XXwF#`_{)MLVCC7P5Ec$$cJnZ} z{_M}My9p;D5q3f-02EZ8t>PF4ni0K`iYnnBsZiwHAVc$QekIAILoP`n*Dqw_$Y$Ta z8*dW6kqohLfe;Bt1rXs6Kg1_Sey1n@-B`jg{SUbRX5R(~ABJ$h$^U>4#9KjlQw@Z# z{9m|`3giq;Vp9C>94#vsOZqWL2uev;<0D z3uGtqYS0q~X{f4ufqhAW0dsv>m+O|J>zG3GiGcg*$8X6@f(cVfEV{?g z7gfy_oZk-GcF##RK(7+y!L)nT0?W+$YL;M{h@Ph7woa!;ip@x9Y~z&Y%l>;Y!Zj~l zWg6Xfc9cv)7b%Cz{VtsTw&2+q5Ia!7q%*;X2RTOp$?zzLqyRC|Bf_6RZHzYW-O%A{EqpCgqpIXr4)l@^lY-a<_%$A*U{D)?u_J^)ol-0%9cQ{) zO;Rw@>P^%~)gI^uOrk zk6lVo#m8#?hzGG6k|5Ir!<$ZgmV-+$0{yta5|~4!|1w8cl!Tr%NnKB5CAe$^c^?wC zbRk4x-d0o!*NUu0K(iLg6iNU6)5^7=RXsjbOoQN)9104^gd!cG)Gh@xay&TAC)WeQ zg^pZOw37y)R7|R?i<;6a;@#0r(mCj4rpB-C(fipfqk@hs?(*X!vx=82$P8((V>j zqA%foO2m9pLYZ40%1Q#A;K7gDFwAXs)*)?795`^HVpDek^m}h(o;4xT2D2heJs9ry z@m(+==5#acE}=|(KtqBvI3J%_>{ZG0)6;V8E$~2)a6k1|&V%a(;3jUmfAaO!ck#4f z%eU08^ZAW}vDNINL76Z@0rA;5#kqM%=6$Fe@_9wG%fnB<@&z$xjF8%-=T$%dWa+7% zyXn$wx5&)1Qk}q$IqM)f441loZH#kh-Ut|J`e7$U3ls@(GI`ZU63FbuaDo)IpVRS�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 aXf!h0zwAQ`vuPH - + {% block additional_meta %} {% endblock additional_meta %} From a0fd697a50dc14e03746b72aa1d032a05d441a51 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 23:52:39 +0100 Subject: [PATCH 10/11] [Tests] delete bittrex ws feed tests --- .../test_unauthenticated_mocked_feeds.py | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 Trading/Exchange/bittrex_websocket_feed/tests/test_unauthenticated_mocked_feeds.py diff --git a/Trading/Exchange/bittrex_websocket_feed/tests/test_unauthenticated_mocked_feeds.py b/Trading/Exchange/bittrex_websocket_feed/tests/test_unauthenticated_mocked_feeds.py deleted file mode 100644 index 3e13eda74..000000000 --- a/Trading/Exchange/bittrex_websocket_feed/tests/test_unauthenticated_mocked_feeds.py +++ /dev/null @@ -1,51 +0,0 @@ -# Drakkar-Software OctoBot-Tentacles -# Copyright (c) Drakkar-Software, All rights reserved. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3.0 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. - -import pytest - -import octobot_commons.channels_name as channels_name -import octobot_commons.enums as commons_enums -import octobot_commons.tests as commons_tests -import octobot_trading.exchanges as exchanges -import octobot_trading.util.test_tools.exchanges_test_tools as exchanges_test_tools -import octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools -from ...bittrex_websocket_feed import BittrexCryptofeedWebsocketConnector - -# All test coroutines will be treated as marked. -pytestmark = pytest.mark.asyncio - - -async def test_start_spot_websocket(): - config = commons_tests.load_test_config() - exchange_manager_instance = await exchanges_test_tools.create_test_exchange_manager( - config=config, exchange_name=BittrexCryptofeedWebsocketConnector.get_name()) - - await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket( - websocket_exchange_class=exchanges.CryptofeedWebSocketExchange, - websocket_connector_class=BittrexCryptofeedWebsocketConnector, - exchange_manager=exchange_manager_instance, - config=config, - symbols=["BTC/USDT", "ETH/USDT"], - time_frames=[commons_enums.TimeFrames.ONE_MINUTE, commons_enums.TimeFrames.ONE_HOUR], - expected_pushed_channels={ - channels_name.OctoBotTradingChannelsName.RECENT_TRADES_CHANNEL.value, - channels_name.OctoBotTradingChannelsName.TICKER_CHANNEL.value, - channels_name.OctoBotTradingChannelsName.KLINE_CHANNEL.value, - channels_name.OctoBotTradingChannelsName.OHLCV_CHANNEL.value, - }, - time_before_assert=20 - ) - await exchanges_test_tools.stop_test_exchange_manager(exchange_manager_instance) From b2f56cb7b56ae478f0be845af974c8f065c4e8b2 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 10 Jan 2024 13:05:17 +0100 Subject: [PATCH 11/11] [GPT] handle creation_error_message --- Services/Services_bases/gpt_service/gpt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Services/Services_bases/gpt_service/gpt.py b/Services/Services_bases/gpt_service/gpt.py index e30d3f8d0..5ed714a61 100644 --- a/Services/Services_bases/gpt_service/gpt.py +++ b/Services/Services_bases/gpt_service/gpt.py @@ -124,6 +124,9 @@ async def _get_signal_from_gpt( raise errors.InvalidRequestError( f"Error when running request with model {model} (invalid request): {err}" ) from err + except openai.AuthenticationError as err: + self.logger.error(f"Invalid OpenAI api key: {err}") + self.creation_error_message = err except Exception as err: raise errors.InvalidRequestError( f"Unexpected error when running request with model {model}: {err}" @@ -314,7 +317,8 @@ async def prepare(self) -> None: self.logger.warning(f"Warning: selected '{self.model}' model is not in GPT available models. " f"Available models are: {self.models}") except openai.AuthenticationError as err: - self.logger.error(f"Error when checking api key: {err}") + self.logger.error(f"Invalid OpenAI api key: {err}") + self.creation_error_message = err except Exception as err: self.logger.error(f"Unexpected error when checking api key: {err}")