Merge branch 'develop' into refactor-informative
This commit is contained in:
commit
6fa8750fea
@ -163,7 +163,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
|
||||
)
|
||||
except TemplateNotFound:
|
||||
selections['exchange'] = render_template(
|
||||
templatefile=f"subtemplates/exchange_generic.j2",
|
||||
templatefile="subtemplates/exchange_generic.j2",
|
||||
arguments=selections
|
||||
)
|
||||
|
||||
|
@ -372,8 +372,8 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"timeframes": Arg(
|
||||
'-t', '--timeframes',
|
||||
help=f'Specify which tickers to download. Space-separated list. '
|
||||
f'Default: `1m 5m`.',
|
||||
help='Specify which tickers to download. Space-separated list. '
|
||||
'Default: `1m 5m`.',
|
||||
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
||||
'6h', '8h', '12h', '1d', '3d', '1w'],
|
||||
default=['1m', '5m'],
|
||||
|
@ -51,7 +51,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
||||
)
|
||||
additional_methods = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/strategy_methods_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/strategy_methods_empty.j2",
|
||||
templatefallbackfile="subtemplates/strategy_methods_empty.j2",
|
||||
)
|
||||
|
||||
strategy_text = render_template(templatefile='base_strategy.py.j2',
|
||||
|
@ -367,8 +367,7 @@ class Exchange:
|
||||
f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
|
||||
|
||||
if timeframe and timeframe_to_minutes(timeframe) < 1:
|
||||
raise OperationalException(
|
||||
f"Timeframes < 1m are currently not supported by Freqtrade.")
|
||||
raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.")
|
||||
|
||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
||||
"""
|
||||
|
@ -874,10 +874,10 @@ class FreqtradeBot:
|
||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||
continue
|
||||
|
||||
trade_state_update = self.update_trade_state(trade, order)
|
||||
fully_cancelled = self.update_trade_state(trade, order)
|
||||
|
||||
if (order['side'] == 'buy' and (
|
||||
trade_state_update
|
||||
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
|
||||
fully_cancelled
|
||||
or self._check_timed_out('buy', order)
|
||||
or strategy_safe_wrapper(self.strategy.check_buy_timeout,
|
||||
default_retval=False)(pair=trade.pair,
|
||||
@ -885,8 +885,8 @@ class FreqtradeBot:
|
||||
order=order))):
|
||||
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
|
||||
elif (order['side'] == 'sell' and (
|
||||
trade_state_update
|
||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
||||
fully_cancelled
|
||||
or self._check_timed_out('sell', order)
|
||||
or strategy_safe_wrapper(self.strategy.check_sell_timeout,
|
||||
default_retval=False)(pair=trade.pair,
|
||||
@ -1121,6 +1121,11 @@ class FreqtradeBot:
|
||||
"""
|
||||
Sends rpc notification when a sell cancel occured.
|
||||
"""
|
||||
if trade.sell_order_status == reason:
|
||||
return
|
||||
else:
|
||||
trade.sell_order_status = reason
|
||||
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
current_rate = self.get_sell_rate(trade.pair, False)
|
||||
|
@ -1,9 +1,6 @@
|
||||
"""
|
||||
Static List provider
|
||||
|
||||
Provides lists as configured in config.json
|
||||
|
||||
"""
|
||||
PairList base class
|
||||
"""
|
||||
import logging
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from copy import deepcopy
|
||||
@ -13,6 +10,7 @@ from cachetools import TTLCache, cached
|
||||
|
||||
from freqtrade.exchange import market_is_active
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -1,14 +1,26 @@
|
||||
"""
|
||||
Precision pair list filter
|
||||
"""
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PrecisionFilter(IPairList):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
# Precalculate sanitized stoploss value to avoid recalculation for every pair
|
||||
self._stoploss = 1 - abs(self._config['stoploss'])
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
@ -31,34 +43,32 @@ class PrecisionFilter(IPairList):
|
||||
:param ticker: ticker dict as returned from ccxt.load_markets()
|
||||
:param stoploss: stoploss value as set in the configuration
|
||||
(already cleaned to be 1 - stoploss)
|
||||
:return: True if the pair can stay, false if it should be removed
|
||||
:return: True if the pair can stay, False if it should be removed
|
||||
"""
|
||||
stop_price = ticker['ask'] * stoploss
|
||||
|
||||
# Adjust stop-prices to precision
|
||||
sp = self._exchange.price_to_precision(ticker["symbol"], stop_price)
|
||||
|
||||
stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99)
|
||||
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
|
||||
|
||||
if sp <= stop_gap_price:
|
||||
self.log_on_refresh(logger.info,
|
||||
f"Removed {ticker['symbol']} from whitelist, "
|
||||
f"because stop price {sp} would be <= stop limit {stop_gap_price}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||
"""
|
||||
Filters and sorts pairlists and assigns and returns them again.
|
||||
"""
|
||||
stoploss = self._config.get('stoploss')
|
||||
if stoploss is not None:
|
||||
# Precalculate sanitized stoploss value to avoid recalculation for every pair
|
||||
stoploss = 1 - abs(stoploss)
|
||||
# Copy list since we're modifying this list
|
||||
for p in deepcopy(pairlist):
|
||||
ticker = tickers.get(p)
|
||||
# Filter out assets which would not allow setting a stoploss
|
||||
if not ticker or (stoploss and not self._validate_precision_filter(ticker, stoploss)):
|
||||
if not self._validate_precision_filter(tickers[p], self._stoploss):
|
||||
pairlist.remove(p)
|
||||
continue
|
||||
|
||||
return pairlist
|
||||
|
@ -1,9 +1,13 @@
|
||||
"""
|
||||
Price pair list filter
|
||||
"""
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -38,14 +42,12 @@ class PriceFilter(IPairList):
|
||||
:return: True if the pair can stay, false if it should be removed
|
||||
"""
|
||||
if ticker['last'] is None:
|
||||
|
||||
self.log_on_refresh(logger.info,
|
||||
f"Removed {ticker['symbol']} from whitelist, because "
|
||||
"ticker['last'] is empty (Usually no trade in the last 24h).")
|
||||
return False
|
||||
compare = ticker['last'] + self._exchange.price_get_one_pip(ticker['symbol'],
|
||||
ticker['last'])
|
||||
changeperc = (compare - ticker['last']) / ticker['last']
|
||||
compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
|
||||
changeperc = compare / ticker['last']
|
||||
if changeperc > self._low_price_ratio:
|
||||
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||
f"because 1 unit is {changeperc * 100:.3f}%")
|
||||
@ -60,14 +62,11 @@ class PriceFilter(IPairList):
|
||||
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||
:return: new whitelist
|
||||
"""
|
||||
# Copy list since we're modifying this list
|
||||
for p in deepcopy(pairlist):
|
||||
ticker = tickers.get(p)
|
||||
if not ticker:
|
||||
pairlist.remove(p)
|
||||
|
||||
# Filter out assets which would not allow setting a stoploss
|
||||
if self._low_price_ratio and not self._validate_ticker_lowprice(ticker):
|
||||
pairlist.remove(p)
|
||||
if self._low_price_ratio:
|
||||
# Copy list since we're modifying this list
|
||||
for p in deepcopy(pairlist):
|
||||
# Filter out assets which would not allow setting a stoploss
|
||||
if not self._validate_ticker_lowprice(tickers[p]):
|
||||
pairlist.remove(p)
|
||||
|
||||
return pairlist
|
||||
|
@ -1,9 +1,13 @@
|
||||
"""
|
||||
Spread pair list filter
|
||||
"""
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -31,8 +35,24 @@ class SpreadFilter(IPairList):
|
||||
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
||||
f"{self._max_spread_ratio * 100}%.")
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||
def _validate_spread(self, ticker: dict) -> bool:
|
||||
"""
|
||||
Validate spread for the ticker
|
||||
:param ticker: ticker dict as returned from ccxt.load_markets()
|
||||
:return: True if the pair can stay, False if it should be removed
|
||||
"""
|
||||
if 'bid' in ticker and 'ask' in ticker:
|
||||
spread = 1 - ticker['bid'] / ticker['ask']
|
||||
if spread > self._max_spread_ratio:
|
||||
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||
f"because spread {spread * 100:.3f}% >"
|
||||
f"{self._max_spread_ratio * 100}%")
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
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
|
||||
@ -41,19 +61,10 @@ class SpreadFilter(IPairList):
|
||||
:return: new whitelist
|
||||
"""
|
||||
# Copy list since we're modifying this list
|
||||
|
||||
spread = None
|
||||
for p in deepcopy(pairlist):
|
||||
ticker = tickers.get(p)
|
||||
assert ticker is not None
|
||||
if 'bid' in ticker and 'ask' in ticker:
|
||||
spread = 1 - ticker['bid'] / ticker['ask']
|
||||
if not ticker or spread > self._max_spread_ratio:
|
||||
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||
f"because spread {spread * 100:.3f}% >"
|
||||
f"{self._max_spread_ratio * 100}%")
|
||||
pairlist.remove(p)
|
||||
else:
|
||||
ticker = tickers[p]
|
||||
# Filter out assets
|
||||
if not self._validate_spread(ticker):
|
||||
pairlist.remove(p)
|
||||
|
||||
return pairlist
|
||||
|
@ -1,14 +1,14 @@
|
||||
"""
|
||||
Static List provider
|
||||
Static Pair List provider
|
||||
|
||||
Provides lists as configured in config.json
|
||||
|
||||
"""
|
||||
Provides pair white list as it configured in config
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
"""
|
||||
Volume PairList provider
|
||||
|
||||
Provides lists as configured in config.json
|
||||
|
||||
"""
|
||||
Provides dynamic pair list based on trade volumes
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
@ -11,8 +10,10 @@ from typing import Any, Dict, List
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
|
||||
|
||||
|
||||
@ -24,8 +25,10 @@ class VolumePairList(IPairList):
|
||||
|
||||
if 'number_assets' not in self._pairlistconfig:
|
||||
raise OperationalException(
|
||||
f'`number_assets` not specified. Please check your configuration '
|
||||
'`number_assets` not specified. Please check your configuration '
|
||||
'for "pairlist.config.number_assets"')
|
||||
|
||||
self._stake_currency = config['stake_currency']
|
||||
self._number_pairs = self._pairlistconfig['number_assets']
|
||||
self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
|
||||
self._min_value = self._pairlistconfig.get('min_value', 0)
|
||||
@ -36,9 +39,11 @@ class VolumePairList(IPairList):
|
||||
'Exchange does not support dynamic whitelist.'
|
||||
'Please edit your config and restart the bot'
|
||||
)
|
||||
|
||||
if not self._validate_keys(self._sort_key):
|
||||
raise OperationalException(
|
||||
f'key {self._sort_key} not in {SORT_VALUES}')
|
||||
|
||||
if self._sort_key != 'quoteVolume':
|
||||
logger.warning(
|
||||
"DEPRECATED: using any key other than quoteVolume for VolumePairList is deprecated."
|
||||
@ -76,42 +81,42 @@ class VolumePairList(IPairList):
|
||||
(self._last_refresh + self.refresh_period < datetime.now().timestamp())):
|
||||
|
||||
self._last_refresh = int(datetime.now().timestamp())
|
||||
pairs = self._gen_pair_whitelist(pairlist, tickers,
|
||||
self._config['stake_currency'],
|
||||
self._sort_key, self._min_value)
|
||||
pairs = self._gen_pair_whitelist(pairlist, tickers)
|
||||
else:
|
||||
pairs = pairlist
|
||||
|
||||
self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}")
|
||||
|
||||
return pairs
|
||||
|
||||
def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict,
|
||||
base_currency: str, key: str, min_val: int) -> List[str]:
|
||||
def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||
"""
|
||||
Updates the whitelist with with a dynamically generated list
|
||||
:param base_currency: base currency as str
|
||||
:param key: sort key (defaults to 'quoteVolume')
|
||||
:param pairlist: pairlist to filter or sort
|
||||
:param tickers: Tickers (from exchange.get_tickers()).
|
||||
:return: List of pairs
|
||||
"""
|
||||
if self._pairlist_pos == 0:
|
||||
# If VolumePairList is the first in the list, use fresh pairlist
|
||||
# Check if pair quote currency equals to the stake currency.
|
||||
filtered_tickers = [v for k, v in tickers.items()
|
||||
if (self._exchange.get_pair_quote_currency(k) == base_currency
|
||||
and v[key] is not None)]
|
||||
filtered_tickers = [
|
||||
v for k, v in tickers.items()
|
||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||
and v[self._sort_key] is not None)]
|
||||
else:
|
||||
# If other pairlist is in front, use the incomming pairlist.
|
||||
# If other pairlist is in front, use the incoming pairlist.
|
||||
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
|
||||
|
||||
if min_val > 0:
|
||||
filtered_tickers = list(filter(lambda t: t[key] > min_val, filtered_tickers))
|
||||
if self._min_value > 0:
|
||||
filtered_tickers = [
|
||||
v for v in filtered_tickers if v[self._sort_key] > self._min_value]
|
||||
|
||||
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key])
|
||||
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key])
|
||||
|
||||
# Validate whitelist to only have active market pairs
|
||||
pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers])
|
||||
pairs = self._verify_blacklist(pairs, aswarning=False)
|
||||
# Limit to X number of pairs
|
||||
# Limit pairlist to the requested number of pairs
|
||||
pairs = pairs[:self._number_pairs]
|
||||
|
||||
return pairs
|
||||
|
@ -1,10 +1,8 @@
|
||||
"""
|
||||
Static List provider
|
||||
|
||||
Provides lists as configured in config.json
|
||||
|
||||
"""
|
||||
PairList manager class
|
||||
"""
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from cachetools import TTLCache, cached
|
||||
@ -83,23 +81,37 @@ class PairListManager():
|
||||
"""
|
||||
Run pairlist through all configured pairlists.
|
||||
"""
|
||||
|
||||
pairlist = self._whitelist.copy()
|
||||
|
||||
# tickers should be cached to avoid calling the exchange on each call.
|
||||
# Tickers should be cached to avoid calling the exchange on each call.
|
||||
tickers: Dict = {}
|
||||
if self._tickers_needed:
|
||||
tickers = self._get_cached_tickers()
|
||||
|
||||
# Adjust whitelist if filters are using tickers
|
||||
pairlist = self._prepare_whitelist(self._whitelist.copy(), tickers)
|
||||
|
||||
# Process all pairlists in chain
|
||||
for pl in self._pairlists:
|
||||
pairlist = pl.filter_pairlist(pairlist, tickers)
|
||||
|
||||
# Validation against blacklist happens after the pairlists to ensure blacklist is respected.
|
||||
# Validation against blacklist happens after the pairlists to ensure
|
||||
# blacklist is respected.
|
||||
pairlist = IPairList.verify_blacklist(pairlist, self.blacklist, True)
|
||||
|
||||
self._whitelist = pairlist
|
||||
|
||||
def _prepare_whitelist(self, pairlist: List[str], tickers) -> List[str]:
|
||||
"""
|
||||
Prepare sanitized pairlist for Pairlist Filters that use tickers data - remove
|
||||
pairs that do not have ticker available
|
||||
"""
|
||||
if self._tickers_needed:
|
||||
# Copy list since we're modifying this list
|
||||
for p in deepcopy(pairlist):
|
||||
if p not in tickers:
|
||||
pairlist.remove(p)
|
||||
|
||||
return pairlist
|
||||
|
||||
def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
|
||||
"""
|
||||
Create list of pair tuples with (pair, ticker_interval)
|
||||
|
@ -86,7 +86,7 @@ def check_migrate(engine) -> None:
|
||||
logger.debug(f'trying {table_back_name}')
|
||||
|
||||
# Check for latest column
|
||||
if not has_column(cols, 'fee_close_cost'):
|
||||
if not has_column(cols, 'sell_order_status'):
|
||||
logger.info(f'Running database migration - backup available as {table_back_name}')
|
||||
|
||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||
@ -113,6 +113,7 @@ def check_migrate(engine) -> None:
|
||||
close_profit_abs = get_column_def(
|
||||
cols, 'close_profit_abs',
|
||||
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
|
||||
sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
|
||||
|
||||
# Schema migration necessary
|
||||
engine.execute(f"alter table trades rename to {table_back_name}")
|
||||
@ -131,7 +132,7 @@ def check_migrate(engine) -> None:
|
||||
stake_amount, amount, open_date, close_date, open_order_id,
|
||||
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||
stoploss_order_id, stoploss_last_update,
|
||||
max_rate, min_rate, sell_reason, strategy,
|
||||
max_rate, min_rate, sell_reason, sell_order_status, strategy,
|
||||
ticker_interval, open_trade_price, close_profit_abs
|
||||
)
|
||||
select id, lower(exchange),
|
||||
@ -153,6 +154,7 @@ def check_migrate(engine) -> None:
|
||||
{initial_stop_loss_pct} initial_stop_loss_pct,
|
||||
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
||||
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
||||
{sell_order_status} sell_order_status,
|
||||
{strategy} strategy, {ticker_interval} ticker_interval,
|
||||
{open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
|
||||
from {table_back_name}
|
||||
@ -228,6 +230,7 @@ class Trade(_DECL_BASE):
|
||||
# Lowest price reached
|
||||
min_rate = Column(Float, nullable=True)
|
||||
sell_reason = Column(String, nullable=True)
|
||||
sell_order_status = Column(String, nullable=True)
|
||||
strategy = Column(String, nullable=True)
|
||||
ticker_interval = Column(Integer, nullable=True)
|
||||
|
||||
@ -267,6 +270,7 @@ class Trade(_DECL_BASE):
|
||||
'stake_amount': round(self.stake_amount, 8),
|
||||
'close_profit': self.close_profit,
|
||||
'sell_reason': self.sell_reason,
|
||||
'sell_order_status': self.sell_order_status,
|
||||
'stop_loss': self.stop_loss,
|
||||
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
||||
'initial_stop_loss': self.initial_stop_loss,
|
||||
@ -370,6 +374,7 @@ class Trade(_DECL_BASE):
|
||||
self.close_profit_abs = self.calc_profit()
|
||||
self.close_date = datetime.utcnow()
|
||||
self.is_open = False
|
||||
self.sell_order_status = 'closed'
|
||||
self.open_order_id = None
|
||||
logger.info(
|
||||
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
||||
|
@ -186,7 +186,7 @@ class RPC:
|
||||
|
||||
def _rpc_daily_profit(
|
||||
self, timescale: int,
|
||||
stake_currency: str, fiat_display_currency: str) -> List[List[Any]]:
|
||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||
today = datetime.utcnow().date()
|
||||
profit_days: Dict[date, Dict] = {}
|
||||
|
||||
@ -206,28 +206,26 @@ class RPC:
|
||||
'trades': len(trades)
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
key,
|
||||
'{value:.8f} {symbol}'.format(
|
||||
value=float(value['amount']),
|
||||
symbol=stake_currency
|
||||
),
|
||||
'{value:.3f} {symbol}'.format(
|
||||
data = [
|
||||
{
|
||||
'date': key,
|
||||
'abs_profit': f'{float(value["amount"]):.8f}',
|
||||
'fiat_value': '{value:.3f}'.format(
|
||||
value=self._fiat_converter.convert_amount(
|
||||
value['amount'],
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
) if self._fiat_converter else 0,
|
||||
symbol=fiat_display_currency
|
||||
),
|
||||
'{value} trade{s}'.format(
|
||||
value=value['trades'],
|
||||
s='' if value['trades'] < 2 else 's'
|
||||
),
|
||||
]
|
||||
'trade_count': f'{value["trades"]}',
|
||||
}
|
||||
for key, value in profit_days.items()
|
||||
]
|
||||
return {
|
||||
'stake_currency': stake_currency,
|
||||
'fiat_display_currency': fiat_display_currency,
|
||||
'data': data
|
||||
}
|
||||
|
||||
def _rpc_trade_history(self, limit: int) -> Dict:
|
||||
""" Returns the X last trades """
|
||||
@ -547,5 +545,5 @@ class RPC:
|
||||
def _rpc_edge(self) -> List[Dict[str, Any]]:
|
||||
""" Returns information related to Edge """
|
||||
if not self._freqtrade.edge:
|
||||
raise RPCException(f'Edge is not enabled.')
|
||||
raise RPCException('Edge is not enabled.')
|
||||
return self._freqtrade.edge.accepted_pairs()
|
||||
|
@ -226,11 +226,15 @@ class Telegram(RPC):
|
||||
# Adding stoploss and stoploss percentage only if it is not None
|
||||
"*Stoploss:* `{stop_loss:.8f}` " +
|
||||
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""),
|
||||
|
||||
"*Open Order:* `{open_order}`" if r['open_order'] else ""
|
||||
]
|
||||
if r['open_order']:
|
||||
if r['sell_order_status']:
|
||||
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
|
||||
else:
|
||||
lines.append("*Open Order:* `{open_order}`")
|
||||
|
||||
# Filter empty lines using list-comprehension
|
||||
messages.append("\n".join([l for l in lines if l]).format(**r))
|
||||
messages.append("\n".join([line for line in lines if line]).format(**r))
|
||||
|
||||
for msg in messages:
|
||||
self._send_msg(msg)
|
||||
@ -276,14 +280,18 @@ class Telegram(RPC):
|
||||
stake_cur,
|
||||
fiat_disp_cur
|
||||
)
|
||||
stats_tab = tabulate(stats,
|
||||
headers=[
|
||||
'Day',
|
||||
f'Profit {stake_cur}',
|
||||
f'Profit {fiat_disp_cur}',
|
||||
f'Trades'
|
||||
],
|
||||
tablefmt='simple')
|
||||
stats_tab = tabulate(
|
||||
[[day['date'],
|
||||
f"{day['abs_profit']} {stats['stake_currency']}",
|
||||
f"{day['fiat_value']} {stats['fiat_display_currency']}",
|
||||
f"{day['trade_count']} trades"] for day in stats['data']],
|
||||
headers=[
|
||||
'Day',
|
||||
f'Profit {stake_cur}',
|
||||
f'Profit {fiat_disp_cur}',
|
||||
'Trades',
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
|
@ -47,9 +47,9 @@ class Webhook(RPC):
|
||||
valuedict = self._config['webhook'].get('webhooksell', None)
|
||||
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhooksellcancel', None)
|
||||
elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION,
|
||||
RPCMessageType.CUSTOM_NOTIFICATION,
|
||||
RPCMessageType.WARNING_NOTIFICATION):
|
||||
elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION,
|
||||
RPCMessageType.CUSTOM_NOTIFICATION,
|
||||
RPCMessageType.WARNING_NOTIFICATION):
|
||||
valuedict = self._config['webhook'].get('webhookstatus', None)
|
||||
else:
|
||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||
|
@ -37,9 +37,7 @@ class Worker:
|
||||
self._heartbeat_msg: float = 0
|
||||
|
||||
# Tell systemd that we completed initialization phase
|
||||
if self._sd_notify:
|
||||
logger.debug("sd_notify: READY=1")
|
||||
self._sd_notify.notify("READY=1")
|
||||
self._notify("READY=1")
|
||||
|
||||
def _init(self, reconfig: bool) -> None:
|
||||
"""
|
||||
@ -60,6 +58,15 @@ class Worker:
|
||||
self._sd_notify = sdnotify.SystemdNotifier() if \
|
||||
self._config.get('internals', {}).get('sd_notify', False) else None
|
||||
|
||||
def _notify(self, message: str) -> None:
|
||||
"""
|
||||
Removes the need to verify in all occurances if sd_notify is enabled
|
||||
:param message: Message to send to systemd if it's enabled.
|
||||
"""
|
||||
if self._sd_notify:
|
||||
logger.debug(f"sd_notify: {message}")
|
||||
self._sd_notify.notify(message)
|
||||
|
||||
def run(self) -> None:
|
||||
state = None
|
||||
while True:
|
||||
@ -89,17 +96,13 @@ class Worker:
|
||||
|
||||
if state == State.STOPPED:
|
||||
# Ping systemd watchdog before sleeping in the stopped state
|
||||
if self._sd_notify:
|
||||
logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.")
|
||||
self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.")
|
||||
self._notify("WATCHDOG=1\nSTATUS=State: STOPPED.")
|
||||
|
||||
self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs)
|
||||
|
||||
elif state == State.RUNNING:
|
||||
# Ping systemd watchdog before throttling
|
||||
if self._sd_notify:
|
||||
logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.")
|
||||
self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
|
||||
self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
|
||||
|
||||
self._throttle(func=self._process_running, throttle_secs=self._throttle_secs)
|
||||
|
||||
@ -154,9 +157,7 @@ class Worker:
|
||||
replaces it with the new instance
|
||||
"""
|
||||
# Tell systemd that we initiated reconfiguration
|
||||
if self._sd_notify:
|
||||
logger.debug("sd_notify: RELOADING=1")
|
||||
self._sd_notify.notify("RELOADING=1")
|
||||
self._notify("RELOADING=1")
|
||||
|
||||
# Clean up current freqtrade modules
|
||||
self.freqtrade.cleanup()
|
||||
@ -167,15 +168,11 @@ class Worker:
|
||||
self.freqtrade.notify_status('config reloaded')
|
||||
|
||||
# Tell systemd that we completed reconfiguration
|
||||
if self._sd_notify:
|
||||
logger.debug("sd_notify: READY=1")
|
||||
self._sd_notify.notify("READY=1")
|
||||
self._notify("READY=1")
|
||||
|
||||
def exit(self) -> None:
|
||||
# Tell systemd that we are exiting now
|
||||
if self._sd_notify:
|
||||
logger.debug("sd_notify: STOPPING=1")
|
||||
self._sd_notify.notify("STOPPING=1")
|
||||
self._notify("STOPPING=1")
|
||||
|
||||
if self.freqtrade:
|
||||
self.freqtrade.notify_status('process died')
|
||||
|
@ -1,7 +1,7 @@
|
||||
# requirements without requirements installable via conda
|
||||
# mainly used for Raspberry pi installs
|
||||
ccxt==1.27.49
|
||||
SQLAlchemy==1.3.16
|
||||
ccxt==1.27.91
|
||||
SQLAlchemy==1.3.17
|
||||
python-telegram-bot==12.7
|
||||
arrow==0.15.6
|
||||
cachetools==4.1.0
|
||||
|
@ -4,7 +4,7 @@
|
||||
-r requirements-hyperopt.txt
|
||||
|
||||
coveralls==2.0.0
|
||||
flake8==3.7.9
|
||||
flake8==3.8.1
|
||||
flake8-type-annotations==0.1.0
|
||||
flake8-tidy-imports==4.1.0
|
||||
mypy==0.770
|
||||
|
@ -1705,7 +1705,7 @@ def hyperopt_results():
|
||||
{
|
||||
'loss': 0.4366182531161519,
|
||||
'params_dict': {
|
||||
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
|
||||
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
|
||||
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
|
||||
'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501
|
||||
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
|
||||
@ -1716,11 +1716,12 @@ def hyperopt_results():
|
||||
}, {
|
||||
'loss': 20.0,
|
||||
'params_dict': {
|
||||
'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
|
||||
'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501
|
||||
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
|
||||
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
|
||||
'stoploss': {'stoploss': -0.338070047333259}},
|
||||
'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
|
||||
'params_details': {
|
||||
'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501
|
||||
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
|
||||
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
|
||||
'stoploss': {'stoploss': -0.338070047333259}},
|
||||
'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501
|
||||
'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501
|
||||
'total_profit': 6.185e-05,
|
||||
@ -1767,8 +1768,9 @@ def hyperopt_results():
|
||||
}, {
|
||||
'loss': 4.713497421432944,
|
||||
'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501
|
||||
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
|
||||
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
|
||||
'params_details': {
|
||||
'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
|
||||
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
|
||||
'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501
|
||||
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501
|
||||
'total_profit': -0.06339929,
|
||||
|
@ -517,9 +517,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||
|
||||
Exchange(default_conf)
|
||||
assert log_has(f"Pair XRP/BTC 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 XRP/BTC from your whitelist.", caplog)
|
||||
assert log_has("Pair XRP/BTC is restricted for some users on this exchange."
|
||||
"Please check if you are impacted by this restriction "
|
||||
"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
|
||||
|
||||
|
||||
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
|
||||
|
@ -555,7 +555,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
||||
"""
|
||||
Buy every xth candle - sell every other xth -2 (hold on to pairs a bit)
|
||||
"""
|
||||
if metadata['pair'] in('ETH/BTC', 'LTC/BTC'):
|
||||
if metadata['pair'] in ('ETH/BTC', 'LTC/BTC'):
|
||||
multi = 20
|
||||
else:
|
||||
multi = 18
|
||||
|
@ -820,7 +820,7 @@ def test_continue_hyperopt(mocker, default_conf, caplog):
|
||||
Hyperopt(default_conf)
|
||||
|
||||
assert unlinkmock.call_count == 0
|
||||
assert log_has(f"Continuing on previous hyperopt results.", caplog)
|
||||
assert log_has("Continuing on previous hyperopt results.", caplog)
|
||||
|
||||
|
||||
def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
|
||||
|
@ -69,44 +69,44 @@ def test_log_on_refresh(mocker, static_pl_conf, markets, tickers):
|
||||
|
||||
|
||||
def test_load_pairlist_noexist(mocker, markets, default_conf):
|
||||
bot = get_patched_freqtradebot(mocker, default_conf)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
plm = PairListManager(bot.exchange, default_conf)
|
||||
plm = PairListManager(freqtrade.exchange, default_conf)
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Impossible to load Pairlist 'NonexistingPairList'. "
|
||||
r"This class does not exist or contains Python code errors."):
|
||||
PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm,
|
||||
PairListResolver.load_pairlist('NonexistingPairList', freqtrade.exchange, plm,
|
||||
default_conf, {}, 1)
|
||||
|
||||
|
||||
def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
# Ensure all except those in whitelist are removed
|
||||
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
|
||||
assert set(whitelist) == set(freqtrade.pairlists.whitelist)
|
||||
# Ensure config dict hasn't been changed
|
||||
assert (static_pl_conf['exchange']['pair_whitelist'] ==
|
||||
freqtradebot.config['exchange']['pair_whitelist'])
|
||||
freqtrade.config['exchange']['pair_whitelist'])
|
||||
|
||||
|
||||
def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
# Ensure all except those in whitelist are removed
|
||||
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
|
||||
assert static_pl_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist
|
||||
assert set(whitelist) == set(freqtrade.pairlists.whitelist)
|
||||
assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
|
||||
|
||||
|
||||
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
|
||||
@ -116,7 +116,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
|
||||
get_tickers=tickers,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
)
|
||||
bot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
# Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -124,9 +124,9 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
|
||||
)
|
||||
# argument: use the whitelist dynamically by exchange-volume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']
|
||||
bot.pairlists.refresh_pairlist()
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
|
||||
assert whitelist == bot.pairlists.whitelist
|
||||
assert whitelist == freqtrade.pairlists.whitelist
|
||||
|
||||
whitelist_conf['pairlists'] = [{'method': 'VolumePairList',
|
||||
'config': {}
|
||||
@ -136,7 +136,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'`number_assets` not specified. Please check your configuration '
|
||||
r'for "pairlist.config.number_assets"'):
|
||||
PairListManager(bot.exchange, whitelist_conf)
|
||||
PairListManager(freqtrade.exchange, whitelist_conf)
|
||||
|
||||
|
||||
def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
@ -144,13 +144,13 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
'freqtrade.exchange.Exchange',
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty))
|
||||
|
||||
# argument: use the whitelist dynamically by exchange-volume
|
||||
whitelist = []
|
||||
whitelist_conf['exchange']['pair_whitelist'] = []
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
pairslist = whitelist_conf['exchange']['pair_whitelist']
|
||||
|
||||
assert set(whitelist) == set(pairslist)
|
||||
@ -206,6 +206,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
|
||||
pairlists, base_currency, whitelist_result,
|
||||
caplog) -> None:
|
||||
whitelist_conf['pairlists'] = pairlists
|
||||
whitelist_conf['stake_currency'] = base_currency
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
@ -215,7 +216,6 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
|
||||
markets=PropertyMock(return_value=shitcoinmarkets),
|
||||
)
|
||||
|
||||
freqtrade.config['stake_currency'] = base_currency
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
whitelist = freqtrade.pairlists.whitelist
|
||||
|
||||
@ -312,18 +312,18 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
)
|
||||
bot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
assert bot.pairlists._pairlists[0]._last_refresh == 0
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
assert freqtrade.pairlists._pairlists[0]._last_refresh == 0
|
||||
assert tickers.call_count == 0
|
||||
bot.pairlists.refresh_pairlist()
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
assert tickers.call_count == 1
|
||||
|
||||
assert bot.pairlists._pairlists[0]._last_refresh != 0
|
||||
lrf = bot.pairlists._pairlists[0]._last_refresh
|
||||
bot.pairlists.refresh_pairlist()
|
||||
assert freqtrade.pairlists._pairlists[0]._last_refresh != 0
|
||||
lrf = freqtrade.pairlists._pairlists[0]._last_refresh
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
assert tickers.call_count == 1
|
||||
# Time should not be updated.
|
||||
assert bot.pairlists._pairlists[0]._last_refresh == lrf
|
||||
assert freqtrade.pairlists._pairlists[0]._last_refresh == lrf
|
||||
|
||||
|
||||
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
|
||||
|
@ -60,6 +60,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'open_trade_price': ANY,
|
||||
'close_rate_requested': ANY,
|
||||
'sell_reason': ANY,
|
||||
'sell_order_status': ANY,
|
||||
'min_rate': ANY,
|
||||
'max_rate': ANY,
|
||||
'strategy': ANY,
|
||||
@ -82,7 +83,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
} == results[0]
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
||||
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
|
||||
results = rpc._rpc_trade_status()
|
||||
assert isnan(results[0]['current_profit'])
|
||||
assert isnan(results[0]['current_rate'])
|
||||
@ -103,6 +104,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'open_trade_price': ANY,
|
||||
'close_rate_requested': ANY,
|
||||
'sell_reason': ANY,
|
||||
'sell_order_status': ANY,
|
||||
'min_rate': ANY,
|
||||
'max_rate': ANY,
|
||||
'strategy': ANY,
|
||||
@ -165,7 +167,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
assert '-0.41% (-0.06)' == result[0][3]
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
||||
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
|
||||
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||
assert 'instantly' == result[0][2]
|
||||
assert 'ETH/BTC' in result[0][1]
|
||||
@ -203,16 +205,18 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||
# Try valid data
|
||||
update.message.text = '/daily 2'
|
||||
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
||||
assert len(days) == 7
|
||||
for day in days:
|
||||
assert len(days['data']) == 7
|
||||
assert days['stake_currency'] == default_conf['stake_currency']
|
||||
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
|
||||
for day in days['data']:
|
||||
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
||||
assert (day[1] == '0.00000000 BTC' or
|
||||
day[1] == '0.00006217 BTC')
|
||||
assert (day['abs_profit'] == '0.00000000' or
|
||||
day['abs_profit'] == '0.00006217')
|
||||
|
||||
assert (day[2] == '0.000 USD' or
|
||||
day[2] == '0.767 USD')
|
||||
assert (day['fiat_value'] == '0.000' or
|
||||
day['fiat_value'] == '0.767')
|
||||
# ensure first day is current date
|
||||
assert str(days[0][0]) == str(datetime.utcnow().date())
|
||||
assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
|
||||
|
||||
# Try invalid data
|
||||
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
||||
@ -315,7 +319,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
|
||||
# Test non-available pair
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
||||
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
|
||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert stats['trade_count'] == 2
|
||||
assert stats['first_trade_date'] == 'just now'
|
||||
|
@ -333,8 +333,10 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
|
||||
)
|
||||
rc = client_get(client, f"{BASE_URI}/daily")
|
||||
assert_response(rc)
|
||||
assert len(rc.json) == 7
|
||||
assert rc.json[0][0] == str(datetime.utcnow().date())
|
||||
assert len(rc.json['data']) == 7
|
||||
assert rc.json['stake_currency'] == 'BTC'
|
||||
assert rc.json['fiat_display_currency'] == 'USD'
|
||||
assert rc.json['data'][0]['date'] == str(datetime.utcnow().date())
|
||||
|
||||
|
||||
def test_api_trades(botclient, mocker, ticker, fee, markets):
|
||||
@ -520,6 +522,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||
'open_rate_requested': 1.098e-05,
|
||||
'open_trade_price': 0.0010025,
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'strategy': 'DefaultStrategy',
|
||||
'ticker_interval': 5}]
|
||||
|
||||
@ -626,6 +629,7 @@ def test_api_forcebuy(botclient, mocker, fee):
|
||||
'open_rate_requested': None,
|
||||
'open_trade_price': 0.2460546025,
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'strategy': None,
|
||||
'ticker_interval': None
|
||||
}
|
||||
|
@ -170,6 +170,7 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None:
|
||||
'current_profit': -0.59,
|
||||
'initial_stop_loss': 1.098e-05,
|
||||
'stop_loss': 1.099e-05,
|
||||
'sell_order_status': None,
|
||||
'initial_stop_loss_pct': -0.05,
|
||||
'stop_loss_pct': -0.01,
|
||||
'open_order': '(limit buy rem=0.00000000)'
|
||||
|
@ -73,7 +73,7 @@ def test_load_config_file_error(default_conf, mocker, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
|
||||
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
|
||||
|
||||
with pytest.raises(OperationalException, match=f".*Please verify the following segment.*"):
|
||||
with pytest.raises(OperationalException, match=r".*Please verify the following segment.*"):
|
||||
load_config_file('somefile')
|
||||
|
||||
|
||||
|
@ -1976,6 +1976,10 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
|
||||
|
||||
Trade.session.add(open_trade)
|
||||
|
||||
# Ensure default is to return empty (so not mocked yet)
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
|
||||
# Return false - trade remains open
|
||||
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
|
||||
freqtrade.check_handle_timedout()
|
||||
@ -2106,6 +2110,9 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_
|
||||
open_trade.is_open = False
|
||||
|
||||
Trade.session.add(open_trade)
|
||||
# Ensure default is false
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
|
||||
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
|
||||
# Return false - No impact
|
||||
@ -2407,30 +2414,47 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
|
||||
def test_handle_cancel_sell_limit(mocker, default_conf) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
|
||||
send_msg_mock = patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
cancel_order=cancel_order_mock
|
||||
cancel_order=cancel_order_mock,
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', return_value=0.245441)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade._notify_sell_cancel = MagicMock()
|
||||
|
||||
trade = MagicMock()
|
||||
trade = Trade(
|
||||
pair='LTC/ETH',
|
||||
amount=2,
|
||||
exchange='binance',
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456",
|
||||
open_date=arrow.utcnow().datetime,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
)
|
||||
order = {'remaining': 1,
|
||||
'amount': 1,
|
||||
'status': "open"}
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
assert freqtrade.handle_cancel_sell(trade, order, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
|
||||
send_msg_mock.reset_mock()
|
||||
|
||||
order['amount'] = 2
|
||||
assert (freqtrade.handle_cancel_sell(trade, order, reason)
|
||||
== CANCEL_REASON['PARTIALLY_FILLED'])
|
||||
assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED']
|
||||
# Assert cancel_order was not called (callcount remains unchanged)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED']
|
||||
# Message should not be iterated again
|
||||
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED']
|
||||
assert send_msg_mock.call_count == 1
|
||||
|
||||
|
||||
def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None:
|
||||
@ -3129,10 +3153,8 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker)
|
||||
caplog.set_level(logging.DEBUG)
|
||||
# Sell as trailing-stop is reached
|
||||
assert freqtrade.handle_trade(trade) is True
|
||||
assert log_has(
|
||||
f"ETH/BTC - HIT STOP: current price at 0.000012, "
|
||||
f"stoploss is 0.000015, "
|
||||
f"initial stoploss was at 0.000010, trade opened at 0.000011", caplog)
|
||||
assert log_has("ETH/BTC - HIT STOP: current price at 0.000012, stoploss is 0.000015, "
|
||||
"initial stoploss was at 0.000010, trade opened at 0.000011", caplog)
|
||||
assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
|
||||
|
||||
|
||||
@ -3175,8 +3197,8 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee,
|
||||
}))
|
||||
# stop-loss not reached, adjusted stoploss
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert log_has(f"ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog)
|
||||
assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog)
|
||||
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert trade.stop_loss == 0.0000138501
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
@ -3232,9 +3254,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
|
||||
}))
|
||||
# stop-loss not reached, adjusted stoploss
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert log_has(f"ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%",
|
||||
caplog)
|
||||
assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%", caplog)
|
||||
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert trade.stop_loss == 0.0000138501
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
@ -3298,7 +3319,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
|
||||
# stop-loss should not be adjusted as offset is not reached yet
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
|
||||
assert not log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert not log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert trade.stop_loss == 0.0000098910
|
||||
|
||||
# price rises above the offset (rises 12% when the offset is 5.5%)
|
||||
@ -3310,9 +3331,8 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
|
||||
}))
|
||||
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert log_has(f"ETH/BTC - Using positive stoploss: 0.05 offset: 0.055 profit: 0.1218%",
|
||||
caplog)
|
||||
assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert log_has("ETH/BTC - Using positive stoploss: 0.05 offset: 0.055 profit: 0.1218%", caplog)
|
||||
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert trade.stop_loss == 0.0000117705
|
||||
|
||||
|
||||
|
@ -477,6 +477,7 @@ def test_migrate_old(mocker, default_conf, fee):
|
||||
assert trade.close_rate_requested is None
|
||||
assert trade.close_rate is not None
|
||||
assert pytest.approx(trade.close_profit_abs) == trade.calc_profit()
|
||||
assert trade.sell_order_status is None
|
||||
|
||||
|
||||
def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
@ -756,6 +757,7 @@ def test_to_json(default_conf, fee):
|
||||
'stake_amount': 0.001,
|
||||
'close_profit': None,
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'stop_loss': None,
|
||||
'stop_loss_pct': None,
|
||||
'initial_stop_loss': None,
|
||||
@ -810,6 +812,7 @@ def test_to_json(default_conf, fee):
|
||||
'open_rate_requested': None,
|
||||
'open_trade_price': 12.33075,
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'strategy': None,
|
||||
'ticker_interval': None}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user