Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Services/Interfaces/web_interface/models/trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ def _dump_order(order, is_simulated):
EXCHANGE: order.exchange_manager.exchange.name if order.exchange_manager else '',
DATE: _convert_timestamp(order.creation_time),
TIME: order.creation_time,
COST: _convert_amount(order.exchange_manager, order.total_cost, market),
COST: _convert_amount(order.exchange_manager, order.total_cost, market) or order.total_cost,
MARKET: market,
SIMULATED_OR_REAL: "Simulated" if is_simulated else "(virtual)" if order.is_self_managed() else "Real",
ID: order.order_id,
Expand Down
1 change: 1 addition & 0 deletions Trading/Exchange/ascendex/ascendex_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class AscendEx(exchanges.RestExchange):

BUY_STR = "Buy"
SELL_STR = "Sell"
SUPPORT_FETCHING_CANCELLED_ORDERS = False

FIX_MARKET_STATUS = True

Expand Down
3 changes: 2 additions & 1 deletion Trading/Exchange/bingx/bingx_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class Bingx(exchanges.RestExchange):
]
# text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)
EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [
('the order is filled or cancelled', )
('the order is filled or cancelled', ''),
('order not exist', '')
]
# text content of errors due to unhandled IP white list issues
EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [
Expand Down
4 changes: 2 additions & 2 deletions Trading/Exchange/bitfinex/bitfinex_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@

class Bitfinex(exchanges.RestExchange):

# bitfinex2 only supports 1, 25 and 100 size
# bitfinex only supports 1, 25 and 100 size
# https://docs.bitfinex.com/reference#rest-public-book
SUPPORTED_ORDER_BOOK_LIMITS = [1, 25, 100]
DEFAULT_ORDER_BOOK_LIMIT = 25
DEFAULT_CANDLE_LIMIT = 500

@classmethod
def get_name(cls):
return 'bitfinex2'
return 'bitfinex'

def get_adapter_class(self):
return BitfinexCCXTAdapter
Expand Down
13 changes: 10 additions & 3 deletions Trading/Exchange/bitmart/bitmart_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
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
Expand All @@ -21,12 +22,18 @@

class BitMartConnector(exchanges.CCXTConnector):

def _client_factory(self, force_unauth, keys_adapter=None) -> tuple:
def _client_factory(
self,
force_unauth,
keys_adapter: typing.Callable[[exchanges.ExchangeCredentialsData], exchanges.ExchangeCredentialsData]=None
) -> tuple:
return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)

def _keys_adapter(self, key, secret, password, uid, auth_token):
def _keys_adapter(self, creds: exchanges.ExchangeCredentialsData) -> exchanges.ExchangeCredentialsData:
# use password as uid
return key, secret, "", password, None, None
creds.uid = creds.password
creds.password = None
return creds


class BitMart(exchanges.RestExchange):
Expand Down
20 changes: 13 additions & 7 deletions Trading/Exchange/coinbase/coinbase_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,24 @@ async def coinbase_retrier_wrapper(*args, **kwargs):

class CoinbaseConnector(ccxt_connector.CCXTConnector):

def _client_factory(self, force_unauth, keys_adapter=None) -> tuple:
def _client_factory(
self,
force_unauth,
keys_adapter: typing.Callable[[exchanges.ExchangeCredentialsData], exchanges.ExchangeCredentialsData]=None
) -> tuple:
return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)

def _keys_adapter(self, key, secret, password, uid, auth_token):
if auth_token:
def _keys_adapter(self, creds: exchanges.ExchangeCredentialsData) -> exchanges.ExchangeCredentialsData:
if creds.auth_token:
# when auth token is provided, force invalid keys
return "ANY_KEY", "ANY_SECRET", password, uid, auth_token, "Bearer "
creds.api_key = "ANY_KEY"
creds.secret = "ANY_KEY"
creds.auth_token_header_prefix = "Bearer "
# CCXT pem key reader is not expecting users to under keys pasted as text from the coinbase UI
# convert \\n to \n to make this format compatible as well
if secret and "\\n" in secret:
secret = secret.replace("\\n", "\n")
return key, secret, password, uid, None, None
if creds.secret and "\\n" in creds.secret:
creds.secret = creds.secret.replace("\\n", "\n")
return creds

@_coinbase_retrier
async def _load_markets(self, client, reload: bool):
Expand Down
1 change: 1 addition & 0 deletions Trading/Exchange/hyperliquid/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .hyperliquid_exchange import Hyperliquid
75 changes: 75 additions & 0 deletions Trading/Exchange/hyperliquid/hyperliquid_exchange.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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 typing

import octobot_trading.exchanges as exchanges
import octobot_trading.enums as trading_enums


class HyperliquidConnector(exchanges.CCXTConnector):

def _client_factory(
self,
force_unauth,
keys_adapter: typing.Callable[[exchanges.ExchangeCredentialsData], exchanges.ExchangeCredentialsData]=None
) -> tuple:
return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)

def _keys_adapter(self, creds: exchanges.ExchangeCredentialsData) -> exchanges.ExchangeCredentialsData:
# use api key and secret as wallet address and private key
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

creds.wallet_address = creds.api_key
creds.private_key = creds.secret
creds.api_key = creds.secret = None
return creds


class Hyperliquid(exchanges.RestExchange):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

DESCRIPTION = ""
DEFAULT_CONNECTOR_CLASS = HyperliquidConnector

FIX_MARKET_STATUS = True
REQUIRE_ORDER_FEES_FROM_TRADES = True # set True when get_order is not giving fees on closed orders and fees
# should be fetched using recent trades.

@classmethod
def get_name(cls):
return 'hyperliquid'

def get_adapter_class(self):
return HyperLiquidCCXTAdapter


class HyperLiquidCCXTAdapter(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

def fix_market_status(self, raw, remove_price_limits=False, **kwargs):
fixed = super().fix_market_status(raw, remove_price_limits=remove_price_limits, **kwargs)
if not fixed:
return fixed
# hyperliquid min cost should be increased by 10% (a few cents above min cost is refused)
limits = fixed[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS.value]
limits[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST.value][
trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST_MIN.value
] = limits[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST.value][
trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST_MIN.value
] * 1.1

return fixed
6 changes: 6 additions & 0 deletions Trading/Exchange/hyperliquid/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": "1.2.0",
"origin_package": "OctoBot-Default-Tentacles",
"tentacles": ["Hyperliquid"],
"tentacles-requirements": []
}
1 change: 1 addition & 0 deletions Trading/Exchange/hyperliquid/resources/hyperliquid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hyperliquid is a basic RestExchange adaptation for Hyperliquid exchange.
15 changes: 15 additions & 0 deletions Trading/Exchange/hyperliquid/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 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.
1 change: 1 addition & 0 deletions Trading/Exchange/hyperliquid_websocket_feed/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .hyperliquid_websocket import HyperliquidCCXTWebsocketConnector
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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.hyperliquid.hyperliquid_exchange as hyperliquid_exchange


class HyperliquidCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):
EXCHANGE_FEEDS = {
Feeds.TRADES: True,
Feeds.KLINE: True,
Feeds.TICKER: True,
Feeds.CANDLE: True,
}

@classmethod
def get_name(cls):
return hyperliquid_exchange.Hyperliquid.get_name()

def get_adapter_class(self, adapter_class):
return hyperliquid_exchange.HyperLiquidCCXTAdapter
6 changes: 6 additions & 0 deletions Trading/Exchange/hyperliquid_websocket_feed/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": "1.2.0",
"origin_package": "OctoBot-Default-Tentacles",
"tentacles": ["HyperliquidCCXTWebsocketConnector"],
"tentacles-requirements": []
}
46 changes: 41 additions & 5 deletions Trading/Exchange/kucoin/kucoin_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import octobot_commons.constants as commons_constants
import octobot_trading.constants as constants
import octobot_trading.enums as trading_enums
import octobot_trading.errors as trading_errors
import octobot.community


Expand Down Expand Up @@ -94,11 +95,11 @@ class Kucoin(exchanges.RestExchange):
# set True when get_positions() is not returning empty positions and should use get_position() instead
REQUIRES_SYMBOL_FOR_EMPTY_POSITION = True

SUPPORTS_SET_MARGIN_TYPE_ON_OPEN_POSITIONS = False # set False when the exchange refuses to change margin type

# get_my_recent_trades only covers the last 24h on kucoin
ALLOW_TRADES_FROM_CLOSED_ORDERS = True # set True when get_my_recent_trades should use get_closed_orders

SUPPORTS_SET_MARGIN_TYPE = False # set False when there is no API to switch between cross and isolated margin types

# should be overridden locally to match exchange support
SUPPORTED_ELEMENTS = {
trading_enums.ExchangeTypes.FUTURE.value: {
Expand Down Expand Up @@ -266,7 +267,7 @@ def get_market_status(self, symbol, price_example=None, with_fixer=True):
async def get_symbol_prices(self, symbol, time_frame, limit: int = 200, **kwargs: dict):
if "since" in kwargs:
# prevent ccxt from fillings the end param (not working when trying to get the 1st candle times)
kwargs["to"] = int(time.time() * 1000)
kwargs["to"] = int(time.time() * commons_constants.MSECONDS_TO_SECONDS)
return await super().get_symbol_prices(symbol, time_frame, limit=limit, **kwargs)

@_kucoin_retrier
Expand Down Expand Up @@ -411,6 +412,24 @@ async def _create_order_with_retry(self, order_type, symbol, quantity: decimal.D
async def get_my_recent_trades(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list:
return await super().get_my_recent_trades(symbol=symbol, since=since, limit=limit, **kwargs)

@_kucoin_retrier
async def set_symbol_margin_type(self, symbol: str, isolated: bool, **kwargs: dict):
"""
Set the symbol margin type
:param symbol: the symbol
:param isolated: when False, margin type is cross, else it's isolated
:return: the update result
"""
try:
return await super().set_symbol_margin_type(symbol, isolated, **kwargs)
except ccxt.errors.ExchangeError as err:
if "Please close or cancel them" in str(err):
if self.SUPPORTS_SET_MARGIN_TYPE_ON_OPEN_POSITIONS:
raise
else:
raise trading_errors.NotSupported(f"set_symbol_margin_type is not supported on open positions")
raise

async def get_position(self, symbol: str, **kwargs: dict) -> dict:
"""
Get the current user symbol position list
Expand Down Expand Up @@ -527,16 +546,33 @@ def _adapt_order_type(self, fixed):
up: Triggers when the price reaches or goes above the stopPrice.
"""
side = fixed.get(trading_enums.ExchangeConstantsOrderColumns.SIDE.value)
# SPOT: trigger_direction can be "loss" or "entry"
# spot
is_stop_loss = False
is_stop_entry = False
trigger_above = False
if trigger_direction in ("up", "loss"):
# spot
if trigger_direction == "loss":
is_stop_loss = True
elif trigger_direction == "entry":
is_stop_entry = True
# futures
elif trigger_direction == "up":
trigger_above = True
elif trigger_direction in ("down", "loss"):
elif trigger_direction == "down":
trigger_above = False
else:
self.logger.error(
f"Unknown [{self.connector.exchange_manager.exchange_name}] order trigger direction "
f"{trigger_direction} ({fixed})"
)
if is_stop_loss:
trigger_above = side == trading_enums.TradeOrderSide.BUY.value
if is_stop_entry:
self.logger.error(
f"Unhandled [{self.connector.exchange_manager.exchange_name}] stop order type "
f"{trigger_direction} ({fixed})"
)
stop_price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value, None)
if side == trading_enums.TradeOrderSide.BUY.value:
if trigger_above:
Expand Down
1 change: 1 addition & 0 deletions Trading/Exchange/wavesexchange/wavesexchange_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

class WavesExchange(exchanges.RestExchange):
DESCRIPTION = ""
FIX_MARKET_STATUS = True
DUMP_INCOMPLETE_LAST_CANDLE = True # set True in tentacle when the exchange can return incomplete last candles

@classmethod
Expand Down