2018-11-30 05:34:56 +00:00
"""
2019-03-02 16:24:28 +00:00
Volume PairList provider
2018-11-30 05:34:56 +00:00
2020-05-17 11:26:21 +00:00
Provides dynamic pair list based on trade volumes
"""
2018-11-30 05:34:56 +00:00
import logging
2021-07-03 09:47:17 +00:00
import arrow
2020-02-02 04:00:40 +00:00
from typing import Any , Dict , List
2019-10-29 09:39:27 +00:00
2021-04-25 18:10:47 +00:00
from cachetools . ttl import TTLCache
2019-12-30 14:02:17 +00:00
from freqtrade . exceptions import OperationalException
2021-07-03 09:39:14 +00:00
from freqtrade . exchange import timeframe_to_minutes
2021-07-03 09:47:17 +00:00
from freqtrade . plugins . pairlist . IPairList import IPairList
2021-07-04 09:16:33 +00:00
from freqtrade . misc import format_ms_time
2020-05-17 11:26:21 +00:00
2018-11-30 05:34:56 +00:00
logger = logging . getLogger ( __name__ )
2020-05-17 11:26:21 +00:00
2020-09-01 06:00:20 +00:00
SORT_VALUES = [ ' quoteVolume ' ]
2018-12-04 06:12:56 +00:00
2018-11-30 05:34:56 +00:00
2018-12-05 19:44:56 +00:00
class VolumePairList ( IPairList ) :
2018-11-30 05:34:56 +00:00
2020-05-20 10:27:07 +00:00
def __init__ ( self , exchange , pairlistmanager ,
config : Dict [ str , Any ] , pairlistconfig : Dict [ str , Any ] ,
2019-11-09 14:04:04 +00:00
pairlist_pos : int ) - > None :
super ( ) . __init__ ( exchange , pairlistmanager , config , pairlistconfig , pairlist_pos )
2019-11-09 05:55:16 +00:00
if ' number_assets ' not in self . _pairlistconfig :
2018-12-06 18:36:33 +00:00
raise OperationalException (
2020-05-18 09:40:25 +00:00
' `number_assets` not specified. Please check your configuration '
2018-12-06 18:36:33 +00:00
' for " pairlist.config.number_assets " ' )
2020-05-15 00:00:55 +00:00
2020-05-15 00:59:13 +00:00
self . _stake_currency = config [ ' stake_currency ' ]
2019-11-09 05:55:16 +00:00
self . _number_pairs = self . _pairlistconfig [ ' number_assets ' ]
self . _sort_key = self . _pairlistconfig . get ( ' sort_key ' , ' quoteVolume ' )
2020-02-03 06:44:17 +00:00
self . _min_value = self . _pairlistconfig . get ( ' min_value ' , 0 )
2021-04-25 18:10:47 +00:00
self . _refresh_period = self . _pairlistconfig . get ( ' refresh_period ' , 1800 )
self . _pair_cache : TTLCache = TTLCache ( maxsize = 1 , ttl = self . _refresh_period )
2021-07-03 09:39:14 +00:00
self . _lookback_days = self . _pairlistconfig . get ( ' lookback_days ' , 0 )
self . _lookback_timeframe = self . _pairlistconfig . get ( ' lookback_timeframe ' , ' 1d ' )
self . _lookback_period = self . _pairlistconfig . get ( ' lookback_period ' , 0 )
2021-07-04 09:16:33 +00:00
if ( self . _lookback_days > 0 ) & ( self . _lookback_period > 0 ) :
raise OperationalException (
f ' Ambigous configuration: lookback_days and lookback_period both set in pairlist config. '
f ' Please set lookback_days only or lookback_period and lookback_timeframe and restart the bot. '
)
2021-07-03 09:39:14 +00:00
# overwrite lookback timeframe and days when lookback_days is set
if self . _lookback_days > 0 :
self . _lookback_timeframe = ' 1d '
self . _lookback_period = self . _lookback_days
2021-07-03 09:47:17 +00:00
# get timeframe in minutes and seconds
2021-07-03 09:39:14 +00:00
self . _tf_in_min = timeframe_to_minutes ( self . _lookback_timeframe )
2021-07-03 09:47:17 +00:00
self . _tf_in_sec = self . _tf_in_min * 60
2021-07-04 09:16:33 +00:00
# wether to use range lookback or not
2021-07-03 09:39:14 +00:00
self . _use_range = ( self . _tf_in_min > 0 ) & ( self . _lookback_period > 0 )
2021-07-03 09:49:05 +00:00
if self . _use_range & ( self . _refresh_period < self . _tf_in_sec ) :
2021-07-03 09:39:14 +00:00
raise OperationalException (
f ' Refresh period of { self . _refresh_period } seconds is smaller than one timeframe of { self . _lookback_timeframe } . '
2021-07-03 09:47:17 +00:00
f ' Please adjust refresh_period to at least { self . _tf_in_sec } and restart the bot. '
2021-07-03 09:39:14 +00:00
)
2018-12-04 06:12:56 +00:00
2019-11-09 05:55:16 +00:00
if not self . _exchange . exchange_has ( ' fetchTickers ' ) :
2018-11-30 19:08:50 +00:00
raise OperationalException (
2020-05-21 09:41:00 +00:00
' Exchange does not support dynamic whitelist. '
' Please edit your config and restart the bot. '
2018-11-30 19:08:50 +00:00
)
2020-05-15 00:00:55 +00:00
2018-12-05 18:48:50 +00:00
if not self . _validate_keys ( self . _sort_key ) :
2018-12-04 06:12:56 +00:00
raise OperationalException (
f ' key { self . _sort_key } not in { SORT_VALUES } ' )
2020-05-15 00:00:55 +00:00
2021-07-03 09:39:14 +00:00
if self . _lookback_period < 0 :
raise OperationalException ( " VolumeFilter requires lookback_period to be >= 0 " )
if self . _lookback_period > exchange . ohlcv_candle_limit ( self . _lookback_timeframe ) :
raise OperationalException ( " VolumeFilter requires lookback_period to not "
" exceed exchange max request size "
f " ( { exchange . ohlcv_candle_limit ( self . _lookback_timeframe ) } ) " )
2019-11-09 06:05:17 +00:00
@property
def needstickers ( self ) - > bool :
"""
Boolean property defining if tickers are necessary .
2020-11-24 19:24:51 +00:00
If no Pairlist requires tickers , an empty Dict is passed
2019-11-09 06:05:17 +00:00
as tickers argument to filter_pairlist
"""
return True
2018-12-05 18:48:50 +00:00
def _validate_keys ( self , key ) :
2018-12-04 06:12:56 +00:00
return key in SORT_VALUES
2018-12-03 19:31:25 +00:00
def short_desc ( self ) - > str :
"""
Short whitelist method description - used for startup - messages
"""
2019-11-09 06:07:33 +00:00
return f " { self . name } - top { self . _pairlistconfig [ ' number_assets ' ] } volume pairs. "
2018-11-30 05:34:56 +00:00
2021-04-25 18:10:47 +00:00
def gen_pairlist ( self , tickers : Dict ) - > List [ str ] :
2018-11-30 05:34:56 +00:00
"""
2020-05-22 12:03:49 +00:00
Generate the pairlist
2021-04-19 13:15:40 +00:00
: param tickers : Tickers ( from exchange . get_tickers ( ) ) . May be cached .
2020-05-22 12:03:49 +00:00
: return : List of pairs
2018-11-30 05:34:56 +00:00
"""
2018-11-30 19:08:50 +00:00
# Generate dynamic whitelist
2020-04-14 18:21:30 +00:00
# Must always run if this pairlist is not the first in the list.
2021-04-25 18:10:47 +00:00
pairlist = self . _pair_cache . get ( ' pairlist ' )
if pairlist :
# Item found - no refresh necessary
return pairlist
else :
2020-05-22 12:03:49 +00:00
# Use fresh pairlist
2020-02-26 06:00:08 +00:00
# Check if pair quote currency equals to the stake currency.
2020-05-15 00:59:13 +00:00
filtered_tickers = [
v for k , v in tickers . items ( )
if ( self . _exchange . get_pair_quote_currency ( k ) == self . _stake_currency
2020-05-16 06:21:36 +00:00
and v [ self . _sort_key ] is not None ) ]
2020-05-22 12:03:49 +00:00
pairlist = [ s [ ' symbol ' ] for s in filtered_tickers ]
2021-04-25 18:10:47 +00:00
pairlist = self . filter_pairlist ( pairlist , tickers )
self . _pair_cache [ ' pairlist ' ] = pairlist
2020-05-22 12:03:49 +00:00
return pairlist
def filter_pairlist ( self , pairlist : List [ str ] , tickers : Dict ) - > List [ str ] :
"""
Filters and sorts pairlist and returns the whitelist again .
Called on each bot iteration - please use internal caching if necessary
: param pairlist : pairlist to filter or sort
: param tickers : Tickers ( from exchange . get_tickers ( ) ) . May be cached .
: return : new whitelist
"""
2021-07-03 09:39:14 +00:00
# Use the incoming pairlist.
2020-05-22 12:03:49 +00:00
filtered_tickers = [ v for k , v in tickers . items ( ) if k in pairlist ]
2019-11-09 14:04:04 +00:00
2021-07-03 09:47:17 +00:00
# get lookback period in ms, for exchange ohlcv fetch
2021-07-03 09:39:14 +00:00
if self . _use_range == True :
since_ms = int ( arrow . utcnow ( )
. floor ( ' minute ' )
. shift ( minutes = - ( self . _lookback_period * self . _tf_in_min ) - self . _tf_in_min )
. float_timestamp ) * 1000
2021-07-04 09:16:33 +00:00
to_ms = int ( arrow . utcnow ( )
. floor ( ' minute ' )
. shift ( minutes = - self . _tf_in_min )
. float_timestamp ) * 1000
# todo: utc date output for starting date
self . log_once ( f " Using volume range of { self . _lookback_period } candles, timeframe: { self . _lookback_timeframe } , starting from { format_ms_time ( since_ms ) } till { format_ms_time ( to_ms ) } " , logger . info )
2021-07-03 09:39:14 +00:00
needed_pairs = [ ( p , self . _lookback_timeframe ) for p in [ s [ ' symbol ' ] for s in filtered_tickers ] if p not in self . _pair_cache ]
2021-07-03 09:47:17 +00:00
2021-07-03 09:39:14 +00:00
# Get all candles
candles = { }
if needed_pairs :
2021-07-03 09:47:17 +00:00
candles = self . _exchange . refresh_latest_ohlcv ( needed_pairs , since_ms = since_ms , cache = False )
2021-07-03 09:39:14 +00:00
for i , p in enumerate ( filtered_tickers ) :
pair_candles = candles [ ( p [ ' symbol ' ] , self . _lookback_timeframe ) ] if ( p [ ' symbol ' ] , self . _lookback_timeframe ) in candles else None
2021-07-03 09:47:17 +00:00
# in case of candle data calculate typical price and quoteVolume for candle
2021-07-03 09:39:14 +00:00
if not pair_candles . empty :
pair_candles [ ' typical_price ' ] = ( pair_candles [ ' high ' ] + pair_candles [ ' low ' ] + pair_candles [ ' close ' ] ) / 3
pair_candles [ ' quoteVolume ' ] = pair_candles [ ' volume ' ] * pair_candles [ ' typical_price ' ]
2021-07-04 09:16:33 +00:00
# replace quoteVolume with range quoteVolume sum calculated above
2021-07-03 09:39:14 +00:00
filtered_tickers [ i ] [ ' quoteVolume ' ] = pair_candles [ ' quoteVolume ' ] . sum ( )
else :
filtered_tickers [ i ] [ ' quoteVolume ' ] = 0
2020-05-15 00:59:13 +00:00
if self . _min_value > 0 :
filtered_tickers = [
v for v in filtered_tickers if v [ self . _sort_key ] > self . _min_value ]
2020-02-03 06:44:17 +00:00
2020-05-15 00:59:13 +00:00
sorted_tickers = sorted ( filtered_tickers , reverse = True , key = lambda t : t [ self . _sort_key ] )
2019-11-09 14:04:04 +00:00
2019-03-02 16:24:28 +00:00
# Validate whitelist to only have active market pairs
2019-11-09 05:55:16 +00:00
pairs = self . _whitelist_for_active_markets ( [ s [ ' symbol ' ] for s in sorted_tickers ] )
2021-07-03 09:47:17 +00:00
pairs = self . verify_blacklist ( pairs , logger . info )
2020-05-17 11:26:21 +00:00
# Limit pairlist to the requested number of pairs
2019-11-09 06:19:46 +00:00
pairs = pairs [ : self . _number_pairs ]
2019-08-18 16:06:36 +00:00
2020-11-22 10:49:41 +00:00
self . log_once ( f " Searching { self . _number_pairs } pairs: { pairs } " , logger . info )
2020-05-22 12:03:49 +00:00
2018-11-30 19:08:50 +00:00
return pairs