Merge pull request #10839 from gaardiolor/hyperliquid

Hyperliquid
This commit is contained in:
Matthias
2024-11-16 11:18:03 +01:00
committed by GitHub
10 changed files with 573 additions and 14 deletions

View File

@@ -33,6 +33,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Bybit](https://bybit.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi)
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [Kraken](https://kraken.com/)
- [X] [OKX](https://okx.com/) (Former OKEX)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
@@ -41,6 +42,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Binance](https://www.binance.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [OKX](https://okx.com/)
- [X] [Bybit](https://bybit.com/)

View File

@@ -303,6 +303,42 @@ It's therefore required to pass the UID as well.
!!! Warning "Necessary Verification"
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
## Hyperliquid
!!! Tip "Stoploss on Exchange"
Hyperliquid supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it.
Hyperliquid is a Decentralized Exchange (DEX). Decentralized exchanges work a bit different compared to normal exchanges. Instead of authenticating private API calls using an API key, private API calls need to be signed with the private key of your wallet.
This needs to be configured like this:
```json
"exchange": {
"name": "hyperliquid",
"walletAddress": "your_eth_wallet_address",
"privateKey": "your_private_key",
// ...
}
```
* walletAddress must be in hex format: `0x<40 hex characters>`, and can be easily copied from your wallet.
* privateKey also must be in hex format: `0x<64 hex characters>`, and can either be exported from your wallet or regenerated using your mnemonic phrase.
Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
!!! Note "Hyperliquid general usage Notes"
Hyperliquid does not support market orders, however ccxt will simulate market orders by placing limit orders with a maximum slippage of 5%.
Unfortunately, hyperliquid only offers 5000 historic candles, so backtesting will either need to build candles historically (by waiting and downloading the data incrementally over time) - or will be limited to the last 5000 candles.
!!! Info "Some general best practices (non exhaustive)"
* Beware of supply chain attacks, like pip package poisoning etcetera. However you export or (re-)generate your private key, make sure your environment is safe.
* Interact as little with the private key as possible. Store it in a separate file from the config.json (secrets.json for example) that you never have to touch, and secure it.
* Always keep your mnemonic phrase and private key private.
* Don't use the same mnemonic as the one you had to backup when initializing a hardware wallet, using the same mnemonic basically deletes the security of your hardware wallet.
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet / private key to trade on Hyperliquid.
* Remember that if someone hacks the host you use for trading, or any other host you stored your private key / mnemonic on, you will lose the funds protected by that private key. That means the funds on that wallet and the funds deposited on Hyperliquid.
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
## All exchanges
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.

View File

@@ -40,11 +40,12 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Binance](https://www.binance.com/)
- [X] [Bitmart](https://bitmart.com/)
- [X] [BingX](https://bingx.com/invite/0EM9RX)
- [X] [Bitmart](https://bitmart.com/)
- [X] [Bybit](https://bybit.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi)
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [Kraken](https://kraken.com/)
- [X] [OKX](https://okx.com/) (Former OKEX)
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
@@ -52,9 +53,10 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
### Supported Futures Exchanges (experimental)
- [X] [Binance](https://www.binance.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [OKX](https://okx.com/)
- [X] [Bybit](https://bybit.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [OKX](https://okx.com/)
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.

View File

@@ -36,6 +36,7 @@ The Order-type will be ignored if only one mode is available.
| Gate | limit |
| Okx | limit |
| Kucoin | stop-limit, stop-market|
| Hyperliquid (futures only) | limit |
!!! Note "Tight stoploss"
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>

View File

@@ -58,6 +58,7 @@ SUPPORTED_EXCHANGES = [
"bybit",
"gate",
"htx",
"hyperliquid",
"kraken",
"okx",
]

View File

@@ -1,8 +1,11 @@
"""Hyperliquid exchange subclass"""
import logging
from datetime import datetime
from freqtrade.enums import TradingMode
from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
@@ -16,20 +19,146 @@ class Hyperliquid(Exchange):
"""
_ft_has: FtHas = {
# Only the most recent 5000 candles are available according to the
# exchange's API documentation.
"ohlcv_has_history": False,
"ohlcv_candle_limit": 5000,
"trades_has_history": False, # Trades endpoint doesn't seem available.
"l2_limit_range": [20],
"trades_has_history": False,
"tickers_have_bid_ask": False,
"stoploss_on_exchange": False,
"exchange_has_overrides": {"fetchTrades": False},
"funding_fee_timeframe": "1h",
"marketOrderRequiresPrice": True,
}
_ft_has_futures: FtHas = {
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit"},
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
@property
def _ccxt_config(self) -> dict:
# Parameters to add directly to ccxt sync/async initialization.
# ccxt defaults to swap mode.
# ccxt Hyperliquid defaults to swap
config = {}
if self.trading_mode == TradingMode.SPOT:
config.update({"options": {"defaultType": "spot"}})
config.update(super()._ccxt_config)
return config
def get_max_leverage(self, pair: str, stake_amount: float | None) -> float:
# There are no leverage tiers
if self.trading_mode == TradingMode.FUTURES:
return self.markets[pair]["limits"]["leverage"]["max"]
else:
return 1.0
def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
) -> int:
# Funding rate candles have a different limit
if candle_type == CandleType.FUNDING_RATE:
return 500
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
if self.trading_mode != TradingMode.SPOT:
# Hyperliquid expects leverage to be an int
leverage = int(leverage)
# Hyperliquid needs the parameter leverage.
# Don't use _set_leverage(), as this sets margin back to cross
self.set_margin_mode(pair, self.margin_mode, params={"leverage": leverage})
def dry_run_liquidation_price(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
amount: float,
stake_amount: float,
leverage: float,
wallet_balance: float, # Or margin balance
open_trades: list,
) -> float | None:
"""
Optimized
Docs: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/liquidations
Below can be done in fewer lines of code, but like this it matches the documentation.
Tested with 196 unique ccxt fetch_positions() position outputs
- Only first output per position where pnl=0.0
- Compare against returned liquidation price
Positions: 197 Average deviation: 0.00028980% Max deviation: 0.01309453%
Positions info:
{'leverage': {1.0: 23, 2.0: 155, 3.0: 8, 4.0: 7, 5.0: 4},
'side': {'long': 133, 'short': 64},
'symbol': {'BTC/USDC:USDC': 81,
'DOGE/USDC:USDC': 20,
'ETH/USDC:USDC': 53,
'SOL/USDC:USDC': 43}}
"""
# Defining/renaming variables to match the documentation
isolated_margin = wallet_balance
position_size = amount
price = open_rate
position_value = price * position_size
max_leverage = self.markets[pair]["limits"]["leverage"]["max"]
# Docs: The maintenance margin is half of the initial margin at max leverage,
# which varies from 3-50x. In other words, the maintenance margin is between 1%
# (for 50x max leverage assets) and 16.7% (for 3x max leverage assets)
# depending on the asset
# The key thing here is 'Half of the initial margin at max leverage'.
# A bit ambiguous, but this interpretation leads to accurate results:
# 1. Start from the position value
# 2. Assume max leverage, calculate the initial margin by dividing the position value
# by the max leverage
# 3. Divide this by 2
maintenance_margin_required = position_value / max_leverage / 2
# Docs: margin_available (isolated) = isolated_margin - maintenance_margin_required
margin_available = isolated_margin - maintenance_margin_required
# Docs: The maintenance margin is half of the initial margin at max leverage
# The docs don't explicitly specify maintenance leverage, but this works.
# Double because of the statement 'half of the initial margin at max leverage'
maintenance_leverage = max_leverage * 2
# Docs: l = 1 / MAINTENANCE_LEVERAGE (Using 'll' to comply with PEP8: E741)
ll = 1 / maintenance_leverage
# Docs: side = 1 for long and -1 for short
side = -1 if is_short else 1
# Docs: liq_price = price - side * margin_available / position_size / (1 - l * side)
liq_price = price - side * margin_available / position_size / (1 - ll * side)
if self.trading_mode == TradingMode.FUTURES:
return liq_price
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading"
)
def get_funding_fees(
self, pair: str, amount: float, is_short: bool, open_date: datetime
) -> float:
"""
Fetch funding fees, either from the exchange (live) or calculates them
based on funding rate/mark price history
:param pair: The quote/base pair of the trade
:param is_short: trade direction
:param amount: Trade amount
:param open_date: Open date of the trade
:return: funding fee since open_date
:raises: ExchangeError if something goes wrong.
"""
# Hyperliquid does not have fetchFundingHistory
if self.trading_mode == TradingMode.FUTURES:
try:
return self._fetch_and_calculate_funding_fees(pair, amount, is_short, open_date)
except ExchangeError:
logger.warning(f"Could not update funding fees for {pair}.")
return 0.0

View File

@@ -4,7 +4,7 @@ bottleneck==1.4.2
numexpr==2.10.1
pandas-ta==0.3.14b
ccxt==4.4.29
ccxt==4.4.31
cryptography==42.0.8; platform_machine == 'armv7l'
cryptography==43.0.3; platform_machine != 'armv7l'
aiohttp==3.10.10

View File

@@ -0,0 +1,374 @@
from datetime import datetime, timezone
from unittest.mock import MagicMock, PropertyMock
import pytest
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange
def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker):
# test if liq price calculated by dry_run_liquidation_price() is close to ccxt liq price
# testing different pairs with large/small prices, different leverages, long, short
markets = {
"BTC/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
"ETH/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
"SOL/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
"DOGE/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
}
positions = [
{
"symbol": "ETH/USDC:USDC",
"entryPrice": 2458.5,
"side": "long",
"contracts": 0.015,
"collateral": 36.864593,
"leverage": 1.0,
"liquidationPrice": 0.86915825,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 63287.0,
"side": "long",
"contracts": 0.00039,
"collateral": 24.673292,
"leverage": 1.0,
"liquidationPrice": 22.37166537,
},
{
"symbol": "SOL/USDC:USDC",
"entryPrice": 146.82,
"side": "long",
"contracts": 0.16,
"collateral": 23.482979,
"leverage": 1.0,
"liquidationPrice": 0.05269872,
},
{
"symbol": "SOL/USDC:USDC",
"entryPrice": 145.83,
"side": "long",
"contracts": 0.33,
"collateral": 24.045107,
"leverage": 2.0,
"liquidationPrice": 74.83696193,
},
{
"symbol": "ETH/USDC:USDC",
"entryPrice": 2459.5,
"side": "long",
"contracts": 0.0199,
"collateral": 24.454895,
"leverage": 2.0,
"liquidationPrice": 1243.0411908,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 62739.0,
"side": "long",
"contracts": 0.00077,
"collateral": 24.137992,
"leverage": 2.0,
"liquidationPrice": 31708.03843631,
},
{
"symbol": "DOGE/USDC:USDC",
"entryPrice": 0.11586,
"side": "long",
"contracts": 437.0,
"collateral": 25.29769,
"leverage": 2.0,
"liquidationPrice": 0.05945697,
},
{
"symbol": "ETH/USDC:USDC",
"entryPrice": 2642.8,
"side": "short",
"contracts": 0.019,
"collateral": 25.091876,
"leverage": 2.0,
"liquidationPrice": 3924.18322043,
},
{
"symbol": "SOL/USDC:USDC",
"entryPrice": 155.89,
"side": "short",
"contracts": 0.32,
"collateral": 24.924941,
"leverage": 2.0,
"liquidationPrice": 228.07847866,
},
{
"symbol": "DOGE/USDC:USDC",
"entryPrice": 0.14333,
"side": "short",
"contracts": 351.0,
"collateral": 25.136807,
"leverage": 2.0,
"liquidationPrice": 0.20970228,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 68595.0,
"side": "short",
"contracts": 0.00069,
"collateral": 23.64871,
"leverage": 2.0,
"liquidationPrice": 101849.99354283,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 65536.0,
"side": "short",
"contracts": 0.00099,
"collateral": 21.604172,
"leverage": 3.0,
"liquidationPrice": 86493.46174617,
},
{
"symbol": "SOL/USDC:USDC",
"entryPrice": 173.06,
"side": "long",
"contracts": 0.6,
"collateral": 20.735658,
"leverage": 5.0,
"liquidationPrice": 142.05186667,
},
{
"symbol": "ETH/USDC:USDC",
"entryPrice": 2545.5,
"side": "long",
"contracts": 0.0329,
"collateral": 20.909894,
"leverage": 4.0,
"liquidationPrice": 1929.23322895,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 67400.0,
"side": "short",
"contracts": 0.00031,
"collateral": 20.887308,
"leverage": 1.0,
"liquidationPrice": 133443.97317151,
},
{
"symbol": "ETH/USDC:USDC",
"entryPrice": 2552.0,
"side": "short",
"contracts": 0.0327,
"collateral": 20.833393,
"leverage": 4.0,
"liquidationPrice": 3157.53150453,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 66930.0,
"side": "long",
"contracts": 0.0015,
"collateral": 20.043862,
"leverage": 5.0,
"liquidationPrice": 54108.51043771,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 67033.0,
"side": "long",
"contracts": 0.00121,
"collateral": 20.251817,
"leverage": 4.0,
"liquidationPrice": 50804.00091827,
},
{
"symbol": "ETH/USDC:USDC",
"entryPrice": 2521.9,
"side": "long",
"contracts": 0.0237,
"collateral": 19.902091,
"leverage": 3.0,
"liquidationPrice": 1699.14071943,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 68139.0,
"side": "short",
"contracts": 0.00145,
"collateral": 19.72573,
"leverage": 5.0,
"liquidationPrice": 80933.61590987,
},
{
"symbol": "SOL/USDC:USDC",
"entryPrice": 178.29,
"side": "short",
"contracts": 0.11,
"collateral": 19.605036,
"leverage": 1.0,
"liquidationPrice": 347.82205322,
},
{
"symbol": "SOL/USDC:USDC",
"entryPrice": 176.23,
"side": "long",
"contracts": 0.33,
"collateral": 19.364946,
"leverage": 3.0,
"liquidationPrice": 120.56240404,
},
{
"symbol": "SOL/USDC:USDC",
"entryPrice": 173.08,
"side": "short",
"contracts": 0.33,
"collateral": 19.01881,
"leverage": 3.0,
"liquidationPrice": 225.08561715,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 68240.0,
"side": "short",
"contracts": 0.00105,
"collateral": 17.887922,
"leverage": 4.0,
"liquidationPrice": 84431.79820839,
},
{
"symbol": "ETH/USDC:USDC",
"entryPrice": 2518.4,
"side": "short",
"contracts": 0.007,
"collateral": 17.62263,
"leverage": 1.0,
"liquidationPrice": 4986.05799151,
},
{
"symbol": "ETH/USDC:USDC",
"entryPrice": 2533.2,
"side": "long",
"contracts": 0.0347,
"collateral": 17.555195,
"leverage": 5.0,
"liquidationPrice": 2047.7642302,
},
{
"symbol": "DOGE/USDC:USDC",
"entryPrice": 0.13284,
"side": "long",
"contracts": 360.0,
"collateral": 15.943218,
"leverage": 3.0,
"liquidationPrice": 0.09082388,
},
{
"symbol": "SOL/USDC:USDC",
"entryPrice": 163.11,
"side": "short",
"contracts": 0.48,
"collateral": 15.650731,
"leverage": 5.0,
"liquidationPrice": 190.94213618,
},
{
"symbol": "BTC/USDC:USDC",
"entryPrice": 67141.0,
"side": "long",
"contracts": 0.00067,
"collateral": 14.979079,
"leverage": 3.0,
"liquidationPrice": 45236.52992613,
},
]
api_mock = MagicMock()
default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated"
default_conf["stake_currency"] = "USDC"
api_mock.load_markets = get_mock_coro(return_value=markets)
exchange = get_patched_exchange(
mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=False
)
for position in positions:
is_short = True if position["side"] == "short" else False
liq_price_returned = position["liquidationPrice"]
liq_price_calculated = exchange.dry_run_liquidation_price(
position["symbol"],
position["entryPrice"],
is_short,
position["contracts"],
position["collateral"],
position["leverage"],
position["collateral"],
[],
)
assert pytest.approx(liq_price_returned, rel=0.0001) == liq_price_calculated
def test_hyperliquid_get_funding_fees(default_conf, mocker):
now = datetime.now(timezone.utc)
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
exchange._fetch_and_calculate_funding_fees = MagicMock()
exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
assert exchange._fetch_and_calculate_funding_fees.call_count == 0
default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated"
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
exchange._fetch_and_calculate_funding_fees = MagicMock()
exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
assert exchange._fetch_and_calculate_funding_fees.call_count == 1
def test_hyperliquid_get_max_leverage(default_conf, mocker):
markets = {
"BTC/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
"ETH/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
"SOL/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
"DOGE/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
}
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
assert exchange.get_max_leverage("BTC/USDC:USDC", 1) == 1.0
default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated"
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
)
assert exchange.get_max_leverage("BTC/USDC:USDC", 1) == 50
assert exchange.get_max_leverage("ETH/USDC:USDC", 20) == 50
assert exchange.get_max_leverage("SOL/USDC:USDC", 50) == 20
assert exchange.get_max_leverage("DOGE/USDC:USDC", 3) == 20
def test_hyperliquid__lev_prep(default_conf, mocker):
api_mock = MagicMock()
api_mock.set_margin_mode = MagicMock()
type(api_mock).has = PropertyMock(return_value={"setMarginMode": True})
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="hyperliquid")
exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
assert api_mock.set_margin_mode.call_count == 0
# test in futures mode
api_mock.set_margin_mode.reset_mock()
default_conf["dry_run"] = False
default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated"
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="hyperliquid")
exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
assert api_mock.set_margin_mode.call_count == 1
api_mock.set_margin_mode.assert_called_with("isolated", "BTC/USDC:USDC", {"leverage": 3})
api_mock.reset_mock()
exchange._lev_prep("BTC/USDC:USDC", 19.99, "sell")
assert api_mock.set_margin_mode.call_count == 1
api_mock.set_margin_mode.assert_called_with("isolated", "BTC/USDC:USDC", {"leverage": 19})

View File

@@ -339,6 +339,18 @@ EXCHANGES = {
},
],
},
"hyperliquid": {
"pair": "PURR/USDC",
"stake_currency": "USDC",
"hasQuoteVolume": False,
"timeframe": "1h",
"futures": True,
"orderbook_max_entries": 20,
"futures_pair": "BTC/USDC:USDC",
"hasQuoteVolumeFutures": True,
"leverage_tiers_public": False,
"leverage_in_spot_market": False,
},
}

View File

@@ -118,9 +118,10 @@ class TestCCXTExchange:
tickers = exch.get_tickers()
assert pair in tickers
assert "ask" in tickers[pair]
assert tickers[pair]["ask"] is not None
assert "bid" in tickers[pair]
assert tickers[pair]["bid"] is not None
if EXCHANGES[exchangename].get("tickers_have_bid_ask"):
assert tickers[pair]["bid"] is not None
assert tickers[pair]["ask"] is not None
assert "quoteVolume" in tickers[pair]
if EXCHANGES[exchangename].get("hasQuoteVolume"):
assert tickers[pair]["quoteVolume"] is not None
@@ -150,9 +151,10 @@ class TestCCXTExchange:
ticker = exch.fetch_ticker(pair)
assert "ask" in ticker
assert ticker["ask"] is not None
assert "bid" in ticker
assert ticker["bid"] is not None
if EXCHANGES[exchangename].get("tickers_have_bid_ask"):
assert ticker["ask"] is not None
assert ticker["bid"] is not None
assert "quoteVolume" in ticker
if EXCHANGES[exchangename].get("hasQuoteVolume"):
assert ticker["quoteVolume"] is not None