Merge branch 'develop' of https://github.com/kecheon/freqtrade into develop

This commit is contained in:
kecheon 2021-04-11 10:19:51 +09:00
commit 7c290baeae
11 changed files with 70 additions and 227 deletions

View File

@ -174,7 +174,7 @@ This filter removes pairs if the average volatility over a `lookback_days` days
This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs.
In the below example:
If the volatility over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h.
If the volatility over the last 10 days is not in the range of 0.05-0.50, remove the pair from the whitelist. The filter is applied every 24h.
```json
"pairlists": [
@ -190,7 +190,7 @@ If the volatility over the last 10 days is not in the range of 0.20-0.30, remove
### Full example of Pairlist Handlers
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value.
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value.
```json
"exchange": {

View File

@ -52,7 +52,7 @@ class Binance(Exchange):
'In stoploss limit order, stop price should be more than limit price')
if self._config['dry_run']:
dry_order = self.dry_run_order(
dry_order = self.create_dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order

View File

@ -543,8 +543,8 @@ class Exchange:
# See also #2575 at github.
return max(min_stake_amounts) * amount_reserve_percent
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, params: Dict = {}) -> Dict[str, Any]:
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, params: Dict = {}) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
_amount = self.amount_to_precision(pair, amount)
dry_order = {
@ -618,7 +618,7 @@ class Exchange:
rate: float, time_in_force: str) -> Dict:
if self._config['dry_run']:
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
dry_order = self.create_dry_run_order(pair, ordertype, "buy", amount, rate)
return dry_order
params = self._params.copy()
@ -631,7 +631,7 @@ class Exchange:
rate: float, time_in_force: str = 'gtc') -> Dict:
if self._config['dry_run']:
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
dry_order = self.create_dry_run_order(pair, ordertype, "sell", amount, rate)
return dry_order
params = self._params.copy()
@ -662,8 +662,6 @@ class Exchange:
@retrier
def get_balance(self, currency: str) -> float:
if self._config['dry_run']:
return self._config['dry_run_wallet']
# ccxt exception is already handled by get_balances
balances = self.get_balances()
@ -675,8 +673,6 @@ class Exchange:
@retrier
def get_balances(self) -> dict:
if self._config['dry_run']:
return {}
try:
balances = self._api.fetch_balance()

View File

@ -53,7 +53,7 @@ class Ftx(Exchange):
stop_price = self.price_to_precision(pair, stop_price)
if self._config['dry_run']:
dry_order = self.dry_run_order(
dry_order = self.create_dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order

View File

@ -92,7 +92,7 @@ class Kraken(Exchange):
stop_price = self.price_to_precision(pair, stop_price)
if self._config['dry_run']:
dry_order = self.dry_run_order(
dry_order = self.create_dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order

View File

@ -410,9 +410,7 @@ class FreqtradeBot(LoggingMixin):
bid_strategy = self.config.get('bid_strategy', {})
if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False):
logger.info(
f"Getting price from order book {bid_strategy['price_side'].capitalize()} side."
)
order_book_top = bid_strategy.get('order_book_top', 1)
order_book = self.exchange.fetch_l2_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book)
@ -425,7 +423,8 @@ class FreqtradeBot(LoggingMixin):
f"Orderbook: {order_book}"
)
raise PricingError from e
logger.info(f'...top {order_book_top} order book buy rate {rate_from_l2:.8f}')
logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side "
f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}")
used_rate = rate_from_l2
else:
logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price")
@ -479,19 +478,17 @@ class FreqtradeBot(LoggingMixin):
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
return False
logger.info(f"Buy signal found: about create a new trade with stake_amount: "
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ...")
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
if self._check_depth_of_market_buy(pair, bid_check_dom):
logger.info(f'Executing Buy for {pair}.')
return self.execute_buy(pair, stake_amount)
else:
return False
logger.info(f'Executing Buy for {pair}')
return self.execute_buy(pair, stake_amount)
else:
return False

View File

@ -4,9 +4,9 @@ e.g BTC to USD
"""
import logging
import time
from typing import Dict, List
from typing import Dict
from cachetools.ttl import TTLCache
from pycoingecko import CoinGeckoAPI
from freqtrade.constants import SUPPORTED_FIAT
@ -15,51 +15,6 @@ from freqtrade.constants import SUPPORTED_FIAT
logger = logging.getLogger(__name__)
class CryptoFiat:
"""
Object to describe what is the price of Crypto-currency in a FIAT
"""
# Constants
CACHE_DURATION = 6 * 60 * 60 # 6 hours
def __init__(self, crypto_symbol: str, fiat_symbol: str, price: float) -> None:
"""
Create an object that will contains the price for a crypto-currency in fiat
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:param price: Price in FIAT
"""
# Public attributes
self.crypto_symbol = None
self.fiat_symbol = None
self.price = 0.0
# Private attributes
self._expiration = 0.0
self.crypto_symbol = crypto_symbol.lower()
self.fiat_symbol = fiat_symbol.lower()
self.set_price(price=price)
def set_price(self, price: float) -> None:
"""
Set the price of the Crypto-currency in FIAT and set the expiration time
:param price: Price of the current Crypto currency in the fiat
:return: None
"""
self.price = price
self._expiration = time.time() + self.CACHE_DURATION
def is_expired(self) -> bool:
"""
Return if the current price is still valid or needs to be refreshed
:return: bool, true the price is expired and needs to be refreshed, false the price is
still valid
"""
return self._expiration - time.time() <= 0
class CryptoToFiatConverter:
"""
Main class to initiate Crypto to FIAT.
@ -84,7 +39,9 @@ class CryptoToFiatConverter:
return CryptoToFiatConverter.__instance
def __init__(self) -> None:
self._pairs: List[CryptoFiat] = []
# Timeout: 6h
self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60)
self._load_cryptomap()
def _load_cryptomap(self) -> None:
@ -118,49 +75,31 @@ class CryptoToFiatConverter:
"""
crypto_symbol = crypto_symbol.lower()
fiat_symbol = fiat_symbol.lower()
inverse = False
if crypto_symbol == 'usd':
# usd corresponds to "uniswap-state-dollar" for coingecko.
# We'll therefore need to "swap" the currencies
logger.info(f"reversing Rates {crypto_symbol}, {fiat_symbol}")
crypto_symbol = fiat_symbol
fiat_symbol = 'usd'
inverse = True
symbol = f"{crypto_symbol}/{fiat_symbol}"
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
# Get the pair that interest us and return the price in fiat
for pair in self._pairs:
if pair.crypto_symbol == crypto_symbol and pair.fiat_symbol == fiat_symbol:
# If the price is expired we refresh it, avoid to call the API all the time
if pair.is_expired():
pair.set_price(
price=self._find_price(
crypto_symbol=pair.crypto_symbol,
fiat_symbol=pair.fiat_symbol
)
)
price = self._pair_price.get(symbol, None)
# return the last price we have for this pair
return pair.price
# The pair does not exist, so we create it and return the price
return self._add_pair(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol,
price=self._find_price(
if not price:
price = self._find_price(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol
)
)
def _add_pair(self, crypto_symbol: str, fiat_symbol: str, price: float) -> float:
"""
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:return: price in FIAT
"""
self._pairs.append(
CryptoFiat(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol,
price=price
)
)
if inverse and price != 0.0:
price = 1 / price
self._pair_price[symbol] = price
return price

View File

@ -931,11 +931,11 @@ def test_exchange_has(default_conf, mocker):
("sell")
])
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_dry_run_order(default_conf, mocker, side, exchange_name):
def test_create_dry_run_order(default_conf, mocker, side, exchange_name):
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
order = exchange.dry_run_order(
order = exchange.create_dry_run_order(
pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200)
assert 'id' in order
assert f'dry_run_{side}_' in order["id"]
@ -1245,14 +1245,6 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
assert "timeInForce" not in api_mock.create_order.call_args[0][5]
def test_get_balance_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
default_conf['dry_run_wallet'] = 999.9
exchange = get_patched_exchange(mocker, default_conf)
assert exchange.get_balance(currency='BTC') == 999.9
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_balance_prod(default_conf, mocker, exchange_name):
api_mock = MagicMock()
@ -1276,13 +1268,6 @@ def test_get_balance_prod(default_conf, mocker, exchange_name):
exchange.get_balance(currency='BTC')
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_balances_dry_run(default_conf, mocker, exchange_name):
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.get_balances() == {}
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_balances_prod(default_conf, mocker, exchange_name):
balance_item = {

View File

@ -1,44 +1,15 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors,
# pragma pylint: disable=protected-access, C0103
import time
from unittest.mock import MagicMock
import pytest
from requests.exceptions import RequestException
from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from tests.conftest import log_has, log_has_re
def test_pair_convertion_object():
pair_convertion = CryptoFiat(
crypto_symbol='btc',
fiat_symbol='usd',
price=12345.0
)
# Check the cache duration is 6 hours
assert pair_convertion.CACHE_DURATION == 6 * 60 * 60
# Check a regular usage
assert pair_convertion.crypto_symbol == 'btc'
assert pair_convertion.fiat_symbol == 'usd'
assert pair_convertion.price == 12345.0
assert pair_convertion.is_expired() is False
# Update the expiration time (- 2 hours) and check the behavior
pair_convertion._expiration = time.time() - 2 * 60 * 60
assert pair_convertion.is_expired() is True
# Check set price behaviour
time_reference = time.time() + pair_convertion.CACHE_DURATION
pair_convertion.set_price(price=30000.123)
assert pair_convertion.is_expired() is False
assert pair_convertion._expiration >= time_reference
assert pair_convertion.price == 30000.123
def test_fiat_convert_is_supported(mocker):
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._is_supported_fiat(fiat='USD') is True
@ -47,28 +18,6 @@ def test_fiat_convert_is_supported(mocker):
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
def test_fiat_convert_add_pair(mocker):
fiat_convert = CryptoToFiatConverter()
pair_len = len(fiat_convert._pairs)
assert pair_len == 0
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0)
pair_len = len(fiat_convert._pairs)
assert pair_len == 1
assert fiat_convert._pairs[0].crypto_symbol == 'btc'
assert fiat_convert._pairs[0].fiat_symbol == 'usd'
assert fiat_convert._pairs[0].price == 12345.0
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2)
pair_len = len(fiat_convert._pairs)
assert pair_len == 2
assert fiat_convert._pairs[1].crypto_symbol == 'btc'
assert fiat_convert._pairs[1].fiat_symbol == 'eur'
assert fiat_convert._pairs[1].price == 13000.2
def test_fiat_convert_find_price(mocker):
fiat_convert = CryptoToFiatConverter()
@ -95,8 +44,8 @@ def test_fiat_convert_unsupported_crypto(mocker, caplog):
def test_fiat_convert_get_price(mocker):
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
return_value=28000.0)
find_price = mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
return_value=28000.0)
fiat_convert = CryptoToFiatConverter()
@ -104,26 +53,17 @@ def test_fiat_convert_get_price(mocker):
fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='US Dollar')
# Check the value return by the method
pair_len = len(fiat_convert._pairs)
pair_len = len(fiat_convert._pair_price)
assert pair_len == 0
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0
assert fiat_convert._pairs[0].crypto_symbol == 'btc'
assert fiat_convert._pairs[0].fiat_symbol == 'usd'
assert fiat_convert._pairs[0].price == 28000.0
assert fiat_convert._pairs[0]._expiration != 0
assert len(fiat_convert._pairs) == 1
assert fiat_convert._pair_price['btc/usd'] == 28000.0
assert len(fiat_convert._pair_price) == 1
assert find_price.call_count == 1
# Verify the cached is used
fiat_convert._pairs[0].price = 9867.543
expiration = fiat_convert._pairs[0]._expiration
fiat_convert._pair_price['btc/usd'] = 9867.543
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 9867.543
assert fiat_convert._pairs[0]._expiration == expiration
# Verify the cache expiration
expiration = time.time() - 2 * 60 * 60
fiat_convert._pairs[0]._expiration = expiration
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0
assert fiat_convert._pairs[0]._expiration is not expiration
assert find_price.call_count == 1
def test_fiat_convert_same_currencies(mocker):

View File

@ -416,10 +416,10 @@ def test_api_count(botclient, mocker, ticker, fee, markets):
assert rc.json()["max"] == 1
# Create some test data
ftbot.enter_positions()
create_mock_trades(fee)
rc = client_get(client, f"{BASE_URI}/count")
assert_response(rc)
assert rc.json()["current"] == 1
assert rc.json()["current"] == 4
assert rc.json()["max"] == 1
ftbot.config['max_open_trades'] = float('inf')
@ -612,7 +612,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
@pytest.mark.usefixtures("init_persistence")
def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order):
def test_api_profit(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient
patch_get_signal(ftbot, (True, False))
mocker.patch.multiple(
@ -627,48 +627,33 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
assert_response(rc, 200)
assert rc.json()['trade_count'] == 0
ftbot.enter_positions()
trade = Trade.query.first()
create_mock_trades(fee)
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
rc = client_get(client, f"{BASE_URI}/profit")
assert_response(rc, 200)
# One open trade
assert rc.json()['trade_count'] == 1
assert rc.json()['best_pair'] == ''
assert rc.json()['best_rate'] == 0
trade = Trade.query.first()
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
rc = client_get(client, f"{BASE_URI}/profit")
assert_response(rc)
assert rc.json() == {'avg_duration': ANY,
'best_pair': 'ETH/BTC',
'best_rate': 6.2,
'first_trade_date': 'just now',
'best_pair': 'XRP/BTC',
'best_rate': 1.0,
'first_trade_date': ANY,
'first_trade_timestamp': ANY,
'latest_trade_date': 'just now',
'latest_trade_date': '5 minutes ago',
'latest_trade_timestamp': ANY,
'profit_all_coin': 6.217e-05,
'profit_all_fiat': 0.76748865,
'profit_all_percent_mean': 6.2,
'profit_all_ratio_mean': 0.06201058,
'profit_all_percent_sum': 6.2,
'profit_all_ratio_sum': 0.06201058,
'profit_closed_coin': 6.217e-05,
'profit_closed_fiat': 0.76748865,
'profit_closed_ratio_mean': 0.06201058,
'profit_closed_percent_mean': 6.2,
'profit_closed_ratio_sum': 0.06201058,
'profit_closed_percent_sum': 6.2,
'trade_count': 1,
'closed_trade_count': 1,
'winning_trades': 1,
'profit_all_coin': -44.0631579,
'profit_all_fiat': -543959.6842755,
'profit_all_percent_mean': -66.41,
'profit_all_ratio_mean': -0.6641100666666667,
'profit_all_percent_sum': -398.47,
'profit_all_ratio_sum': -3.9846604,
'profit_closed_coin': 0.00073913,
'profit_closed_fiat': 9.124559849999999,
'profit_closed_ratio_mean': 0.0075,
'profit_closed_percent_mean': 0.75,
'profit_closed_ratio_sum': 0.015,
'profit_closed_percent_sum': 1.5,
'trade_count': 6,
'closed_trade_count': 2,
'winning_trades': 2,
'losing_trades': 0,
}

View File

@ -685,7 +685,8 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy
assert trade.amount == 91.07468123
assert log_has(
'Buy signal found: about create a new trade with stake_amount: 0.001 ...', caplog
'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...',
caplog
)