Repository: freqtrade/freqtrade
Pull request: Support non pairs
## Summary
Certain markets are not in the format of quote/base.

This PR allows to extract either side from the markets dict for each pair.


Closes #268
Closes #1107 (the bot should now support inversed pairs too)
most likely will close #2969 (still needs validation / confirmation)


## Quick changelog

- move pairlist validation for correct stake currency to exchange (it requires markets now)
- remove all usages of `pair.startswith()`, `pair.endswith()` and `pair.split('/')` and replace them with the methods `get_pair_quote_currency()` and `get_pair_base_currency()`
- Remove config validation to make sure pairs are `base/quote` formatted.


Problem statement:
Refactoring pair name handling
Code is crippled with` pair.replace('_', '-'),` what about adding a method in the exchange class that defines the delimiter and remove `pair.replace` in all the code ? 

FT hard links wallet and base pair 
In any margin or futures exchange balance will more than likely be in the pair not the base currency. 

As example for BTC/USD - USD is the base currency. 
But in a margin market the wallet will be BTC

This produces an error in FT as Wallet and Base are assumed to be the same, this a false expectation in trading.

As example in bitmex, FT will not start as bitmex returns the wallet but FT is expecting only USD as the base.
```
2018-08-01 06:14:26,575 - freqtrade.exchange - WARNING - get_balance() returned exception: "Could not get USD balance due to malformed exchange response: {'BTC': {'free': 2.285, 'used': 0.0, 'total': 2.285}}"
2018-08-01 06:14:26,575 - freqtrade.exchange - WARNING - retrying get_balance() still for 2 times
```

FTX - Future markets - Error with stake currency on PERP contracts
All futures on FTX trade against USD and are called in the format of COIN-PERP; 

I'm getting this error when trying to trade on dry-run with futures using ALGO-PERP

```2020-02-24 19:22:14,270 - freqtrade.loggers - INFO - Verbosity set to 0
2020-02-24 19:22:14,270 - freqtrade.configuration.configuration - INFO - Dry run is enabled
2020-02-24 19:22:14,270 - freqtrade.configuration.configuration - INFO - Using DB: "sqlite:///ftx-futures.sqlite"
2020-02-24 19:22:14,271 - freqtrade.configuration.configuration - INFO - Using max_open_trades: 50 ...
2020-02-24 19:22:14,271 - freqtrade.configuration.configuration - INFO - Using user-data directory: /home/dante/freqtrade/user_data ...
2020-02-24 19:22:14,271 - freqtrade.configuration.configuration - INFO - Using data directory: /home/dante/freqtrade/user_data/data/ftx ...
2020-02-24 19:22:14,271 - freqtrade.configuration.check_exchange - INFO - Checking exchange...
2020-02-24 19:22:14,271 - freqtrade.configuration.check_exchange - WARNING - Exchange "ftx" is known to the the ccxt library, available for the bot, but not officially supported by the Freqtrade development team. It may work flawlessly (please report back) or have serious issues. Use it at your own discretion.
2020-02-24 19:22:14,271 - freqtrade.configuration.configuration - INFO - Using pairlist from configuration.
2020-02-24 19:22:14,271 - freqtrade.configuration.deprecated_settings - WARNING - DEPRECATED: The `experimental.use_sell_signal` setting is deprecated and will be removed in the next versions of Freqtrade. Please use the `ask_strategy.use_sell_signal` setting in your configuration instead.
2020-02-24 19:22:14,271 - freqtrade.configuration.deprecated_settings - WARNING - DEPRECATED: The `experimental.sell_profit_only` setting is deprecated and will be removed in the next versions of Freqtrade. Please use the `ask_strategy.sell_profit_only` setting in your configuration instead.
2020-02-24 19:22:14,271 - freqtrade.configuration.deprecated_settings - WARNING - DEPRECATED: The `experimental.ignore_roi_if_buy_signal` setting is deprecated and will be removed in the next versions of Freqtrade. Please use the `ask_strategy.ignore_roi_if_buy_signal` setting in your configuration instead.
2020-02-24 19:22:14,271 - freqtrade.configuration.deprecated_settings - WARNING - DEPRECATED: Pairlists must be defined explicitly in the future.Defaulting to StaticPairList for now.
2020-02-24 19:22:14,272 - freqtrade.freqtradebot - INFO - Starting freqtrade develop-221950cd
2020-02-24 19:22:14,286 - freqtrade.resolvers.iresolver - INFO - Using resolved strategy ao2 from '/home/dante/freqtrade/user_data/strategies/ao2.py'...
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Override strategy 'ticker_interval' with value in config file: 4h.
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Override strategy 'stake_currency' with value in config file: USD.
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Override strategy 'stake_amount' with value in config file: 2.
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Override strategy 'unfilledtimeout' with value in config file: {'buy': 30, 'sell': 30}.
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Override strategy 'use_sell_signal' with value in config file: True.
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Override strategy 'sell_profit_only' with value in config file: False.
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Override strategy 'ignore_roi_if_buy_signal' with value in config file: False.
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using minimal_roi: {'0': 3}
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using ticker_interval: 4h
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using stoploss: -0.99
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using trailing_stop: False
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using trailing_stop_positive: 0.2
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using trailing_stop_positive_offset: 0.2
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using trailing_only_offset_is_reached: True
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using process_only_new_candles: True
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using order_types: {'buy': 'market', 'sell': 'market', 'stoploss': 'market', 'stoploss_on_exchange': False}
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using order_time_in_force: {'buy': 'gtc', 'sell': 'gtc'}
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using stake_currency: USD
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using stake_amount: 2
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using startup_candle_count: 0
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using unfilledtimeout: {'buy': 30, 'sell': 30}
2020-02-24 19:22:14,286 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using use_sell_signal: True
2020-02-24 19:22:14,287 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using sell_profit_only: False
2020-02-24 19:22:14,287 - freqtrade.resolvers.strategy_resolver - INFO - Strategy using ignore_roi_if_buy_signal: False
2020-02-24 19:22:14,287 - freqtrade - ERROR - Stake-currency 'USD' not compatible with pair-whitelist. Please remove the following pairs: ['ALGO/PERP']```

Hints from issue discussion:
Sounds good to me
after much thinking, I think the name of the pair should be automatically created by the bot. We define the **stake_currency** as for example BTC, and the list of pairs needs to be only something like **[ "XVG" ,  "XRP" ]**. The bot can then form the correct pair
Thats a good idea, it also gives more flexibility when switching the stake_currency on a regular base.
This issue consists of 2 diff. parts: the original message and the proposal in the comment (5 Jun 2018).

First one is not actual for long time: mapping of symbols in the pair names is only made when the filename is generated.

Second one is better be refused: there can be more than one market on the exchange with the same quote currency and our base (stake) currency.

Funnily, i've been looking at this issue just this week, and came to a different conclusion.

`pair.replace("/", "_")` is used twice (3 times) - once to saveand load data, and once as plot filename - so i didn't see it as anything that needs action (we've had this code-segment around 20 times in the code - mostly in places where it wasn't needed) - and that's when this issue was created.

For the 2nd part - i've had the dream to create a Pair class (my experiments around that are almost 6 months old ...).
I think it would simplify a lot of things ... we'd have `pair.quote`, or `pair.as_filename` or ...

I've played around with that part a bit and i think it would work - all while supporting both `XRP/BTC` and `XRP` (+ stake currency) configuration in the pairlist (as proposed above) - which would actually be very easy to do if it's a class creation.

The part i didn't investigate yet is how to integrate that with the database (maybe `str(pair)` ...) but we're not creating new trades that often, so i guess that would work fine, too.


First part is completely not actual for the current code base, right -- I guess it was actual more than an year ago, when the codebase contained many such pair name conversions, it's not an issue anymore.

The second proposal:

No problem with generation of the pairs from the stake currency symbol and symbols for pairs. With a class as you mentioned, or in any other way. The point here is that the markets (which symbols do not contain '/') can also be tradable -- like options and futures. An exchange may also contain "pairs" (in our terms, how we define them -- a market with '/' in the symbol name) with same quote currency -- like "ETH_H19/BTC" and "ETH_M20/BTC". And I think that support for trading futures and options is more important than this proposed ability to specify the list of quote currencies instead of current list of pairs/market symbols (in the future). So if we decide to move to this direction, the current pair lists (lists of market symbols in the future) should be kept, it cannot be replaced with the new proposed list of currency symbols. If the list of currencies will be implemented, the current list of pairs/markets should also be kept. And this can be too complex for users, to have both approaches supported in the configuration.

I'm not sure i understand the point about the same quote currency...

>  "ETH_H19/BTC" and "ETH_M20/BTC".

you could specify these as "ETH_H19" and "ETH_M20" - quote-currency is BTC - no? - so this would still work ... ? 

> And this can be too complex for users, to have both approaches supported in the configuration.

this is a good point ... as said, i experimented about possibilities - but did not implement a single line within freqtrade regarding this yet... 


> you could specify these as "ETH_H19" and "ETH_M20" - quote-currency is BTC - no? - so this would still work ... ?

In the current aproach yes, but 1) this wont allow to trade markets with no slash in the symbol, 2) I think that quote/base currency should actually be taken from the ccxt market info dict, which contains 'quote' and 'base' items. So all current `pair.endswith(stake_currency)` should be changed to `get_base_currency(market, stake_currency)`, where `get_base_currency()` checks the base currency against ccxt market info dict, not against the trailing part of the pair(market) symbol name. Same for qoute currency (the `startswith` checks). This functionality already exists, after introducing `list-markets` command, the usage of this should simply be refined. This will easily allow to use options/futures market symbols that do not contain slashes in the names.

I don;t think its easy at the stage the bot is right now to go for leveraged/futures/options. Although this is something i would love as well.
There are lots of changes that need to be made basically from the config to the way the bot handles ccxt interaction with the exchanges, going thru the profit and stop loss handling.
I don;t think this was intended to work with futures.
no definitely not, but that doesn't mean we can work towards this long-term

not something we can easily implement quick - that's for sure
The reason here is the naming - as these pairs are not in the format of `quote/stake`.

It's not a bug (it's something not supported so far) - but supporting this is a new feature - as we've not supported such pairs until now.
I have a solution btw (partly in stashes, but should be checked and changed everywhere in the code) -- against `starts_with` and `ends_with` we should call the functions that fetch base currency and quote currency (we already have them, they were made for `list-markets` command) and check against stake currency/quote currency, where it's needed...

the problematic part is the ends_with .. and that's only occuring in 2 places - the 2nd is odd to get by where it is.

That's the problem with your "stashes" - nothing is finished - which creates double work ... :) you should really try to finish these stashes off ... 
ah, they mostly keep some small experimental changes, that are far from completion... as a backlog for ideas...

Commit message: Fix last occurances of pair splitting

Patch to review:
diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py
index 5183ad0b4ef..5ba7ff2947f 100644
--- a/freqtrade/configuration/config_validation.py
+++ b/freqtrade/configuration/config_validation.py
@@ -150,15 +150,3 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
         if (pl.get('method') == 'StaticPairList'
                 and not conf.get('exchange', {}).get('pair_whitelist')):
             raise OperationalException("StaticPairList requires pair_whitelist to be set.")
-
-        if pl.get('method') == 'StaticPairList':
-            stake = conf['stake_currency']
-            invalid_pairs = []
-            for pair in conf['exchange'].get('pair_whitelist'):
-                if not pair.endswith(f'/{stake}'):
-                    invalid_pairs.append(pair)
-
-            if invalid_pairs:
-                raise OperationalException(
-                    f"Stake-currency '{stake}' not compatible with pair-whitelist. "
-                    f"Please remove the following pairs: {invalid_pairs}")
diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index 105cd6b531f..1504d1f1cdc 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -251,7 +251,6 @@
                     'type': 'array',
                     'items': {
                         'type': 'string',
-                        'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
                     },
                     'uniqueItems': True
                 },
@@ -259,7 +258,6 @@
                     'type': 'array',
                     'items': {
                         'type': 'string',
-                        'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
                     },
                     'uniqueItems': True
                 },
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index b3b347016e3..6964986b0e6 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -228,6 +228,18 @@ def get_quote_currencies(self) -> List[str]:
         markets = self.markets
         return sorted(set([x['quote'] for _, x in markets.items()]))
 
+    def get_pair_quote_currency(self, pair: str) -> str:
+        """
+        Return a pair's quote currency
+        """
+        return self.markets.get(pair, {}).get('quote')
+
+    def get_pair_base_currency(self, pair: str) -> str:
+        """
+        Return a pair's quote currency
+        """
+        return self.markets.get(pair, {}).get('base')
+
     def klines(self, pair_interval: Tuple[str, str], copy: bool = True) -> DataFrame:
         if pair_interval in self._klines:
             return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
@@ -300,7 +312,7 @@ def validate_pairs(self, pairs: List[str]) -> None:
         if not self.markets:
             logger.warning('Unable to validate pairs (assuming they are correct).')
             return
-
+        invalid_pairs = []
         for pair in pairs:
             # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
             # TODO: add a support for having coins in BTC/USDT format
@@ -322,6 +334,12 @@ def validate_pairs(self, pairs: List[str]) -> None:
                 logger.warning(f"Pair {pair} is restricted for some users on this exchange."
                                f"Please check if you are impacted by this restriction "
                                f"on the exchange and eventually remove {pair} from your whitelist.")
+            if not self.markets[pair].get('quote') == self._config['stake_currency']:
+                invalid_pairs.append(pair)
+        if invalid_pairs:
+            raise OperationalException(
+                f"Stake-currency '{self._config['stake_currency']}' not compatible with "
+                f"pair-whitelist. Please remove the following pairs: {invalid_pairs}")
 
     def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
         """
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 00d5c369aed..424a6a22072 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -938,8 +938,7 @@ def _safe_sell_amount(self, pair: str, amount: float) -> float:
         """
         # Update wallets to ensure amounts tied up in a stoploss is now free!
         self.wallets.update()
-
-        wallet_amount = self.wallets.get_free(pair.split('/')[0])
+        wallet_amount = self.wallets.get_free(self.exchange.get_pair_base_currency(pair))
         logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}")
         if wallet_amount >= amount:
             return amount
@@ -1125,12 +1124,13 @@ def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None)
         if trade.fee_open == 0 or order['status'] == 'open':
             return order_amount
 
+        trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
         # use fee from order-dict if possible
         if ('fee' in order and order['fee'] is not None and
                 (order['fee'].keys() >= {'currency', 'cost'})):
             if (order['fee']['currency'] is not None and
                     order['fee']['cost'] is not None and
-                    trade.pair.startswith(order['fee']['currency'])):
+                    trade_base_currency == order['fee']['currency']):
                 new_amount = order_amount - order['fee']['cost']
                 logger.info("Applying fee on amount for %s (from %s to %s) from Order",
                             trade, order['amount'], new_amount)
@@ -1145,6 +1145,7 @@ def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None)
             return order_amount
         amount = 0
         fee_abs = 0
+        trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
         for exectrade in trades:
             amount += exectrade['amount']
             if ("fee" in exectrade and exectrade['fee'] is not None and
@@ -1152,7 +1153,7 @@ def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None)
                 # only applies if fee is in quote currency!
                 if (exectrade['fee']['currency'] is not None and
                         exectrade['fee']['cost'] is not None and
-                        trade.pair.startswith(exectrade['fee']['currency'])):
+                        trade_base_currency == exectrade['fee']['currency']):
                     fee_abs += exectrade['fee']['cost']
 
         if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py
index 1ad4da52347..7d489ece7f1 100644
--- a/freqtrade/pairlist/IPairList.py
+++ b/freqtrade/pairlist/IPairList.py
@@ -99,7 +99,8 @@ def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]:
                 logger.warning(f"Pair {pair} is not compatible with exchange "
                                f"{self._exchange.name}. Removing it from whitelist..")
                 continue
-            if not pair.endswith(self._config['stake_currency']):
+
+            if markets[pair]['quote'] != self._config['stake_currency']:
                 logger.warning(f"Pair {pair} is not compatible with your stake currency "
                                f"{self._config['stake_currency']}. Removing it from whitelist..")
                 continue
diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py
index e50dafb6317..e1cdb4a43c1 100644
--- a/freqtrade/pairlist/VolumePairList.py
+++ b/freqtrade/pairlist/VolumePairList.py
@@ -91,9 +91,9 @@ def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict,
 
         if self._pairlist_pos == 0:
             # If VolumePairList is the first in the list, use fresh pairlist
-            # check length so that we make sure that '/' is actually in the string
+            # check base currency equals to stake currency.
             filtered_tickers = [v for k, v in tickers.items()
-                                if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency
+                                if (self._exchange.get_pair_quote_currency(k) == base_currency
                                     and v[key] is not None)]
         else:
             # If other pairlist is in front, use the incomming pairlist.
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 3411318bb0a..d680d36b1ef 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -462,7 +462,7 @@ def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
 
         # Check pair is in stake currency
         stake_currency = self._freqtrade.config.get('stake_currency')
-        if not pair.endswith(stake_currency):
+        if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
             raise RPCException(
                 f'Wrong pair selected. Please pairs with stake {stake_currency} pairs only')
         # check if valid pair
@@ -517,7 +517,7 @@ def _rpc_blacklist(self, add: List[str] = None) -> Dict:
         if add:
             stake_currency = self._freqtrade.config.get('stake_currency')
             for pair in add:
-                if (pair.endswith(stake_currency)
+                if (self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency
                         and pair not in self._freqtrade.pairlists.blacklist):
                     self._freqtrade.pairlists.blacklist.append(pair)
 
diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py
index dd5e34fe6c8..b913155bc33 100644
--- a/freqtrade/wallets.py
+++ b/freqtrade/wallets.py
@@ -74,7 +74,7 @@ def _update_dry(self) -> None:
         )
 
         for trade in open_trades:
-            curr = trade.pair.split('/')[0]
+            curr = self._exchange.get_pair_base_currency(trade.pair)
             _wallets[curr] = Wallet(
                 curr,
                 trade.amount,
diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py
index a9fe0f63706..fd8df4b5661 100644
--- a/tests/commands/test_commands.py
+++ b/tests/commands/test_commands.py
@@ -217,8 +217,9 @@ def test_list_markets(mocker, markets, capsys):
     ]
     start_list_markets(get_args(args), False)
     captured = capsys.readouterr()
-    assert ("Exchange Bittrex has 9 active markets: "
-            "BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n"
+    assert ("Exchange Bittrex has 10 active markets: "
+            "BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, NEO/BTC, "
+            "TKN/BTC, XLTCUSDT, XRP/BTC.\n"
             in captured.out)
 
     patch_exchange(mocker, api_mock=api_mock, id="binance")
@@ -231,7 +232,7 @@ def test_list_markets(mocker, markets, capsys):
     pargs['config'] = None
     start_list_markets(pargs, False)
     captured = capsys.readouterr()
-    assert re.match("\nExchange Binance has 9 active markets:\n",
+    assert re.match("\nExchange Binance has 10 active markets:\n",
                     captured.out)
 
     patch_exchange(mocker, api_mock=api_mock, id="bittrex")
@@ -243,8 +244,8 @@ def test_list_markets(mocker, markets, capsys):
     ]
     start_list_markets(get_args(args), False)
     captured = capsys.readouterr()
-    assert ("Exchange Bittrex has 11 markets: "
-            "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, LTC/USDT, NEO/BTC, "
+    assert ("Exchange Bittrex has 12 markets: "
+            "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, "
             "TKN/BTC, XLTCUSDT, XRP/BTC.\n"
             in captured.out)
 
@@ -256,8 +257,8 @@ def test_list_markets(mocker, markets, capsys):
     ]
     start_list_markets(get_args(args), True)
     captured = capsys.readouterr()
-    assert ("Exchange Bittrex has 8 active pairs: "
-            "BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, NEO/BTC, TKN/BTC, XRP/BTC.\n"
+    assert ("Exchange Bittrex has 9 active pairs: "
+            "BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XRP/BTC.\n"
             in captured.out)
 
     # Test list-pairs subcommand with --all: all pairs
@@ -268,8 +269,8 @@ def test_list_markets(mocker, markets, capsys):
     ]
     start_list_markets(get_args(args), True)
     captured = capsys.readouterr()
-    assert ("Exchange Bittrex has 10 pairs: "
-            "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, LTC/USDT, NEO/BTC, "
+    assert ("Exchange Bittrex has 11 pairs: "
+            "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, "
             "TKN/BTC, XRP/BTC.\n"
             in captured.out)
 
@@ -282,8 +283,8 @@ def test_list_markets(mocker, markets, capsys):
     ]
     start_list_markets(get_args(args), False)
     captured = capsys.readouterr()
-    assert ("Exchange Bittrex has 5 active markets with ETH, LTC as base currencies: "
-            "ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, XLTCUSDT.\n"
+    assert ("Exchange Bittrex has 6 active markets with ETH, LTC as base currencies: "
+            "ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n"
             in captured.out)
 
     # active markets, base=LTC
@@ -295,8 +296,8 @@ def test_list_markets(mocker, markets, capsys):
     ]
     start_list_markets(get_args(args), False)
     captured = capsys.readouterr()
-    assert ("Exchange Bittrex has 3 active markets with LTC as base currency: "
-            "LTC/BTC, LTC/USD, XLTCUSDT.\n"
+    assert ("Exchange Bittrex has 4 active markets with LTC as base currency: "
+            "LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n"
             in captured.out)
 
     # active markets, quote=USDT, USD
@@ -384,7 +385,7 @@ def test_list_markets(mocker, markets, capsys):
     ]
     start_list_markets(get_args(args), False)
     captured = capsys.readouterr()
-    assert ("Exchange Bittrex has 9 active markets:\n"
+    assert ("Exchange Bittrex has 10 active markets:\n"
             in captured.out)
 
     # Test tabular output, no markets found
@@ -407,7 +408,7 @@ def test_list_markets(mocker, markets, capsys):
     ]
     start_list_markets(get_args(args), False)
     captured = capsys.readouterr()
-    assert ('["BLK/BTC","ETH/BTC","ETH/USDT","LTC/BTC","LTC/USD","NEO/BTC",'
+    assert ('["BLK/BTC","ETH/BTC","ETH/USDT","LTC/BTC","LTC/ETH","LTC/USD","NEO/BTC",'
             '"TKN/BTC","XLTCUSDT","XRP/BTC"]'
             in captured.out)
 
diff --git a/tests/conftest.py b/tests/conftest.py
index acb730330ea..000f628683b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -575,7 +575,34 @@ def get_markets():
                 }
             },
             'info': {},
-        }
+        },
+        'LTC/ETH': {
+            'id': 'LTCETH',
+            'symbol': 'LTC/ETH',
+            'base': 'LTC',
+            'quote': 'ETH',
+            'active': True,
+            'precision': {
+                'base': 8,
+                'quote': 8,
+                'amount': 3,
+                'price': 5
+            },
+            'limits': {
+                'amount': {
+                    'min': 0.001,
+                    'max': 10000000.0
+                },
+                'price': {
+                    'min': 1e-05,
+                    'max': 1000.0
+                },
+                'cost': {
+                    'min': 0.01,
+                    'max': None
+                }
+            },
+        },
     }
 
 
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index 8b2e439c37c..ca2bedb0100 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -400,13 +400,38 @@ def test_validate_stake_currency_error(default_conf, mocker, caplog):
 def test_get_quote_currencies(default_conf, mocker):
     ex = get_patched_exchange(mocker, default_conf)
 
-    assert set(ex.get_quote_currencies()) == set(['USD', 'BTC', 'USDT'])
+    assert set(ex.get_quote_currencies()) == set(['USD', 'ETH', 'BTC', 'USDT'])
+
+
+@pytest.mark.parametrize('pair,expected', [
+    ('XRP/BTC', 'BTC'),
+    ('LTC/USD', 'USD'),
+    ('ETH/USDT', 'USDT'),
+    ('XLTCUSDT', 'USDT'),
+])
+def test_get_pair_quote_currency(default_conf, mocker, pair, expected):
+    ex = get_patched_exchange(mocker, default_conf)
+    assert ex.get_pair_quote_currency(pair) == expected
+
+
+@pytest.mark.parametrize('pair,expected', [
+    ('XRP/BTC', 'XRP'),
+    ('LTC/USD', 'LTC'),
+    ('ETH/USDT', 'ETH'),
+    ('XLTCUSDT', 'LTC'),
+])
+def test_get_pair_base_currency(default_conf, mocker, pair, expected):
+    ex = get_patched_exchange(mocker, default_conf)
+    assert ex.get_pair_base_currency(pair) == expected
 
 
 def test_validate_pairs(default_conf, mocker):  # test exchange.validate_pairs directly
     api_mock = MagicMock()
     type(api_mock).markets = PropertyMock(return_value={
-        'ETH/BTC': {}, 'LTC/BTC': {}, 'XRP/BTC': {}, 'NEO/BTC': {}
+        'ETH/BTC': {'quote': 'BTC'},
+        'LTC/BTC': {'quote': 'BTC'},
+        'XRP/BTC': {'quote': 'BTC'},
+        'NEO/BTC': {'quote': 'BTC'},
     })
     id_mock = PropertyMock(return_value='test_exchange')
     type(api_mock).id = id_mock
@@ -454,9 +479,9 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
 def test_validate_pairs_restricted(default_conf, mocker, caplog):
     api_mock = MagicMock()
     type(api_mock).markets = PropertyMock(return_value={
-        'ETH/BTC': {}, 'LTC/BTC': {},
-        'XRP/BTC': {'info': {'IsRestricted': True}},
-        'NEO/BTC': {'info': 'TestString'},  # info can also be a string ...
+        'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
+        'XRP/BTC': {'quote': 'BTC', 'info': {'IsRestricted': True}},
+        'NEO/BTC': {'quote': 'BTC', 'info': 'TestString'},  # info can also be a string ...
     })
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
     mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
@@ -469,6 +494,38 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
                    f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
 
 
+def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
+    api_mock = MagicMock()
+    type(api_mock).markets = PropertyMock(return_value={
+        'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
+        'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'},
+        'HELLO-WORLD': {'quote': 'BTC'},
+    })
+    mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
+
+    Exchange(default_conf)
+
+
+def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
+    default_conf['exchange']['pair_whitelist'].append('HELLO-WORLD')
+    api_mock = MagicMock()
+    type(api_mock).markets = PropertyMock(return_value={
+        'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
+        'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'},
+        'HELLO-WORLD': {'quote': 'USDT'},
+    })
+    mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
+
+    with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"):
+        Exchange(default_conf)
+
+
 @pytest.mark.parametrize("timeframe", [
     ('5m'), ("1m"), ("15m"), ("1h")
 ])
@@ -1828,6 +1885,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
         # 'ETH/BTC':  'active': True
         # 'ETH/USDT': 'active': True
         # 'LTC/BTC':  'active': False
+        # 'LTC/ETH':  'active': True
         # 'LTC/USD':  'active': True
         # 'LTC/USDT': 'active': True
         # 'NEO/BTC':  'active': False
@@ -1836,26 +1894,26 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
         # 'XRP/BTC':  'active': False
         # all markets
         ([], [], False, False,
-         ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD',
+         ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
           'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']),
         # active markets
         ([], [], False, True,
-         ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'NEO/BTC',
+         ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC',
           'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']),
         # all pairs
         ([], [], True, False,
-         ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD',
+         ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
           'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']),
         # active pairs
         ([], [], True, True,
-         ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'NEO/BTC',
+         ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC',
           'TKN/BTC', 'XRP/BTC']),
         # all markets, base=ETH, LTC
         (['ETH', 'LTC'], [], False, False,
-         ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']),
+         ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']),
         # all markets, base=LTC
         (['LTC'], [], False, False,
-         ['LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']),
+         ['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']),
         # all markets, quote=USDT
         ([], ['USDT'], False, False,
          ['ETH/USDT', 'LTC/USDT', 'XLTCUSDT']),
diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
index a35bfa0d60f..3c99e6b929c 100644
--- a/tests/rpc/test_rpc.py
+++ b/tests/rpc/test_rpc.py
@@ -687,7 +687,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None
 
     # Test buy pair not with stakes
     with pytest.raises(RPCException, match=r'Wrong pair selected. Please pairs with stake.*'):
-        rpc._rpc_forcebuy('XRP/ETH', 0.0001)
+        rpc._rpc_forcebuy('LTC/ETH', 0.0001)
     pair = 'XRP/BTC'
 
     # Test not buying
diff --git a/tests/test_configuration.py b/tests/test_configuration.py
index d810305db24..a58e88ea0f0 100644
--- a/tests/test_configuration.py
+++ b/tests/test_configuration.py
@@ -34,13 +34,6 @@ def all_conf():
     return conf
 
 
-def test_load_config_invalid_pair(default_conf) -> None:
-    default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
-
-    with pytest.raises(ValidationError, match=r'.*does not match.*'):
-        validate_config_schema(default_conf)
-
-
 def test_load_config_missing_attributes(default_conf) -> None:
     conf = deepcopy(default_conf)
     conf.pop('exchange')
@@ -810,12 +803,6 @@ def test_validate_whitelist(default_conf):
 
     validate_config_consistency(conf)
 
-    conf = deepcopy(default_conf)
-    conf['stake_currency'] = 'USDT'
-    with pytest.raises(OperationalException,
-                       match=r"Stake-currency 'USDT' not compatible with pair-whitelist.*"):
-        validate_config_consistency(conf)
-
 
 def test_load_config_test_comments() -> None:
     """
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 20db46fac66..9a7816d35ea 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2192,6 +2192,7 @@ def test_handle_timedout_limit_buy(mocker, default_conf, limit_buy_order) -> Non
 
     Trade.session = MagicMock()
     trade = MagicMock()
+    trade.pair = 'LTC/ETH'
     limit_buy_order['remaining'] = limit_buy_order['amount']
     assert freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
     assert cancel_order_mock.call_count == 1
@@ -2215,6 +2216,7 @@ def test_handle_timedout_limit_buy_corder_empty(mocker, default_conf, limit_buy_
 
     Trade.session = MagicMock()
     trade = MagicMock()
+    trade.pair = 'LTC/ETH'
     limit_buy_order['remaining'] = limit_buy_order['amount']
     assert freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
     assert cancel_order_mock.call_count == 1


Code review comments:
diff_hunk: @@ -322,6 +334,12 @@ def validate_pairs(self, pairs: List[str]) -> None:
                 logger.warning(f"Pair {pair} is restricted for some users on this exchange."
                                f"Please check if you are impacted by this restriction "
                                f"on the exchange and eventually remove {pair} from your whitelist.")
+            if not self.markets[pair].get('quote') == self._config['stake_currency']:
path: freqtrade/exchange/exchange.py
text: @user1:
`self.get_pair_quote_currency()` should better be called here?

diff_hunk: @@ -91,9 +91,9 @@ def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict,
 
         if self._pairlist_pos == 0:
             # If VolumePairList is the first in the list, use fresh pairlist
-            # check length so that we make sure that '/' is actually in the string
+            # check base currency equals to stake currency.
path: freqtrade/pairlist/VolumePairList.py
text: @user1:
```suggestion
            # Check if pair quote currency equals to the stake currency.
```
-- it checks quote curr, not base.

The name of the var should also be changes, since it's confusing now

diff_hunk: @@ -462,7 +462,7 @@ def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
 
         # Check pair is in stake currency
path: freqtrade/rpc/rpc.py
text: @user1:
```suggestion
        # Check if pair quote currency equals to the stake currency.
```

diff_hunk: @@ -99,7 +99,8 @@ def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]:
                 logger.warning(f"Pair {pair} is not compatible with exchange "
                                f"{self._exchange.name}. Removing it from whitelist..")
                 continue
-            if not pair.endswith(self._config['stake_currency']):
+
+            if markets[pair]['quote'] != self._config['stake_currency']:
path: freqtrade/pairlist/IPairList.py
text: @user1:
`self.get_pair_quote_currency()` should better be called here?

diff_hunk: @@ -1145,14 +1145,15 @@ def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None)
             return order_amount
         amount = 0
         fee_abs = 0
+        trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
path: freqtrade/freqtradebot.py
text: @user1:
already assigned above?


@author:
Just testing how good the review is :laughing: :+1: 

great catch :+1: 

@user1:
🤣

diff_hunk: @@ -938,8 +938,7 @@ def _safe_sell_amount(self, pair: str, amount: float) -> float:
         """
         # Update wallets to ensure amounts tied up in a stoploss is now free!
         self.wallets.update()
-
-        wallet_amount = self.wallets.get_free(pair.split('/')[0])
+        wallet_amount = self.wallets.get_free(self.exchange.get_pair_base_currency(pair))
path: freqtrade/freqtradebot.py
text: @user1:
```suggestion
        trade_base_currency = self.exchange.get_pair_base_currency(pair)
        wallet_amount = self.wallets.get_free(trade_base_currency)
```
-- this `trade_base_currency` can later be used for logging; code unified with that used in `get_real_amount` below...

@author:
I distrust changes done via "commit suggestion" directly when it touches code.

 typo - and then good look figuring it out (it's all online ...) ;) thats why i often prefer to do the change "manually" - offline in an editor ...

Also - when some include suggested changes, while other comments don't - it'd create a ton of commits ... which is often unnecessary :( 

(sorry for the rant about suggested change) - often it's good though :+1: 

@user1:
Agree, but this only an easy format to "suggest" the change. you do not have to commit the suggestion, it can be done, yes, manually with the checks before committing...
👍