Merge branch 'develop' into refactor-informative

This commit is contained in:
hroff-1902 2020-05-18 14:00:09 +03:00 committed by GitHub
commit 6fa8750fea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 290 additions and 209 deletions

View File

@ -163,7 +163,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
) )
except TemplateNotFound: except TemplateNotFound:
selections['exchange'] = render_template( selections['exchange'] = render_template(
templatefile=f"subtemplates/exchange_generic.j2", templatefile="subtemplates/exchange_generic.j2",
arguments=selections arguments=selections
) )

View File

@ -372,8 +372,8 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"timeframes": Arg( "timeframes": Arg(
'-t', '--timeframes', '-t', '--timeframes',
help=f'Specify which tickers to download. Space-separated list. ' help='Specify which tickers to download. Space-separated list. '
f'Default: `1m 5m`.', 'Default: `1m 5m`.',
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
'6h', '8h', '12h', '1d', '3d', '1w'], '6h', '8h', '12h', '1d', '3d', '1w'],
default=['1m', '5m'], default=['1m', '5m'],

View File

@ -51,7 +51,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
) )
additional_methods = render_template_with_fallback( additional_methods = render_template_with_fallback(
templatefile=f"subtemplates/strategy_methods_{subtemplate}.j2", 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', strategy_text = render_template(templatefile='base_strategy.py.j2',

View File

@ -367,8 +367,7 @@ class Exchange:
f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}") f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
if timeframe and timeframe_to_minutes(timeframe) < 1: if timeframe and timeframe_to_minutes(timeframe) < 1:
raise OperationalException( raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.")
f"Timeframes < 1m are currently not supported by Freqtrade.")
def validate_ordertypes(self, order_types: Dict) -> None: def validate_ordertypes(self, order_types: Dict) -> None:
""" """

View File

@ -874,10 +874,10 @@ class FreqtradeBot:
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue continue
trade_state_update = self.update_trade_state(trade, order) fully_cancelled = self.update_trade_state(trade, order)
if (order['side'] == 'buy' and ( if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
trade_state_update fully_cancelled
or self._check_timed_out('buy', order) or self._check_timed_out('buy', order)
or strategy_safe_wrapper(self.strategy.check_buy_timeout, or strategy_safe_wrapper(self.strategy.check_buy_timeout,
default_retval=False)(pair=trade.pair, default_retval=False)(pair=trade.pair,
@ -885,8 +885,8 @@ class FreqtradeBot:
order=order))): order=order))):
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT'])
elif (order['side'] == 'sell' and ( elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
trade_state_update fully_cancelled
or self._check_timed_out('sell', order) or self._check_timed_out('sell', order)
or strategy_safe_wrapper(self.strategy.check_sell_timeout, or strategy_safe_wrapper(self.strategy.check_sell_timeout,
default_retval=False)(pair=trade.pair, default_retval=False)(pair=trade.pair,
@ -1121,6 +1121,11 @@ class FreqtradeBot:
""" """
Sends rpc notification when a sell cancel occured. 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_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
current_rate = self.get_sell_rate(trade.pair, False) current_rate = self.get_sell_rate(trade.pair, False)

View File

@ -1,9 +1,6 @@
""" """
Static List provider PairList base class
"""
Provides lists as configured in config.json
"""
import logging import logging
from abc import ABC, abstractmethod, abstractproperty from abc import ABC, abstractmethod, abstractproperty
from copy import deepcopy from copy import deepcopy
@ -13,6 +10,7 @@ from cachetools import TTLCache, cached
from freqtrade.exchange import market_is_active from freqtrade.exchange import market_is_active
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,14 +1,26 @@
"""
Precision pair list filter
"""
import logging import logging
from copy import deepcopy from copy import deepcopy
from typing import Dict, List from typing import Any, Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PrecisionFilter(IPairList): 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 @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
""" """
@ -31,34 +43,32 @@ class PrecisionFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:param stoploss: stoploss value as set in the configuration :param stoploss: stoploss value as set in the configuration
(already cleaned to be 1 - stoploss) (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 stop_price = ticker['ask'] * stoploss
# Adjust stop-prices to precision # Adjust stop-prices to precision
sp = self._exchange.price_to_precision(ticker["symbol"], stop_price) sp = self._exchange.price_to_precision(ticker["symbol"], stop_price)
stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99) stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99)
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
if sp <= stop_gap_price: if sp <= stop_gap_price:
self.log_on_refresh(logger.info, self.log_on_refresh(logger.info,
f"Removed {ticker['symbol']} from whitelist, " f"Removed {ticker['symbol']} from whitelist, "
f"because stop price {sp} would be <= stop limit {stop_gap_price}") f"because stop price {sp} would be <= stop limit {stop_gap_price}")
return False return False
return True return True
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Filters and sorts pairlists and assigns and returns them again. 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 # Copy list since we're modifying this list
for p in deepcopy(pairlist): for p in deepcopy(pairlist):
ticker = tickers.get(p)
# Filter out assets which would not allow setting a stoploss # 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) pairlist.remove(p)
continue
return pairlist return pairlist

View File

@ -1,9 +1,13 @@
"""
Price pair list filter
"""
import logging import logging
from copy import deepcopy from copy import deepcopy
from typing import Any, Dict, List from typing import Any, Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -38,14 +42,12 @@ class PriceFilter(IPairList):
: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
""" """
if ticker['last'] is None: if ticker['last'] is None:
self.log_on_refresh(logger.info, self.log_on_refresh(logger.info,
f"Removed {ticker['symbol']} from whitelist, because " f"Removed {ticker['symbol']} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).") "ticker['last'] is empty (Usually no trade in the last 24h).")
return False return False
compare = ticker['last'] + self._exchange.price_get_one_pip(ticker['symbol'], compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
ticker['last']) changeperc = compare / ticker['last']
changeperc = (compare - ticker['last']) / ticker['last']
if changeperc > self._low_price_ratio: if changeperc > self._low_price_ratio:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because 1 unit is {changeperc * 100:.3f}%") 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. :param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist :return: new whitelist
""" """
# Copy list since we're modifying this list if self._low_price_ratio:
for p in deepcopy(pairlist): # Copy list since we're modifying this list
ticker = tickers.get(p) for p in deepcopy(pairlist):
if not ticker: # Filter out assets which would not allow setting a stoploss
pairlist.remove(p) if not self._validate_ticker_lowprice(tickers[p]):
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)
return pairlist return pairlist

View File

@ -1,9 +1,13 @@
"""
Spread pair list filter
"""
import logging import logging
from copy import deepcopy from copy import deepcopy
from typing import Dict, List from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,8 +35,24 @@ class SpreadFilter(IPairList):
return (f"{self.name} - Filtering pairs with ask/bid diff above " return (f"{self.name} - Filtering pairs with ask/bid diff above "
f"{self._max_spread_ratio * 100}%.") 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. Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary Called on each bot iteration - please use internal caching if necessary
@ -41,19 +61,10 @@ class SpreadFilter(IPairList):
:return: new whitelist :return: new whitelist
""" """
# Copy list since we're modifying this list # Copy list since we're modifying this list
spread = None
for p in deepcopy(pairlist): for p in deepcopy(pairlist):
ticker = tickers.get(p) ticker = tickers[p]
assert ticker is not None # Filter out assets
if 'bid' in ticker and 'ask' in ticker: if not self._validate_spread(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:
pairlist.remove(p) pairlist.remove(p)
return pairlist return pairlist

View File

@ -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 import logging
from typing import Dict, List from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,9 +1,8 @@
""" """
Volume PairList provider Volume PairList provider
Provides lists as configured in config.json Provides dynamic pair list based on trade volumes
"""
"""
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List from typing import Any, Dict, List
@ -11,8 +10,10 @@ from typing import Any, Dict, List
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
@ -24,8 +25,10 @@ class VolumePairList(IPairList):
if 'number_assets' not in self._pairlistconfig: if 'number_assets' not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
f'`number_assets` not specified. Please check your configuration ' '`number_assets` not specified. Please check your configuration '
'for "pairlist.config.number_assets"') 'for "pairlist.config.number_assets"')
self._stake_currency = config['stake_currency']
self._number_pairs = self._pairlistconfig['number_assets'] self._number_pairs = self._pairlistconfig['number_assets']
self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume') self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
self._min_value = self._pairlistconfig.get('min_value', 0) self._min_value = self._pairlistconfig.get('min_value', 0)
@ -36,9 +39,11 @@ class VolumePairList(IPairList):
'Exchange does not support dynamic whitelist.' 'Exchange does not support dynamic whitelist.'
'Please edit your config and restart the bot' 'Please edit your config and restart the bot'
) )
if not self._validate_keys(self._sort_key): if not self._validate_keys(self._sort_key):
raise OperationalException( raise OperationalException(
f'key {self._sort_key} not in {SORT_VALUES}') f'key {self._sort_key} not in {SORT_VALUES}')
if self._sort_key != 'quoteVolume': if self._sort_key != 'quoteVolume':
logger.warning( logger.warning(
"DEPRECATED: using any key other than quoteVolume for VolumePairList is deprecated." "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 + self.refresh_period < datetime.now().timestamp())):
self._last_refresh = int(datetime.now().timestamp()) self._last_refresh = int(datetime.now().timestamp())
pairs = self._gen_pair_whitelist(pairlist, tickers, pairs = self._gen_pair_whitelist(pairlist, tickers)
self._config['stake_currency'],
self._sort_key, self._min_value)
else: else:
pairs = pairlist pairs = pairlist
self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}") self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}")
return pairs return pairs
def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict, def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict) -> List[str]:
base_currency: str, key: str, min_val: int) -> List[str]:
""" """
Updates the whitelist with with a dynamically generated list Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str :param pairlist: pairlist to filter or sort
:param key: sort key (defaults to 'quoteVolume')
:param tickers: Tickers (from exchange.get_tickers()). :param tickers: Tickers (from exchange.get_tickers()).
:return: List of pairs :return: List of pairs
""" """
if self._pairlist_pos == 0: if self._pairlist_pos == 0:
# If VolumePairList is the first in the list, use fresh pairlist # If VolumePairList is the first in the list, use fresh pairlist
# Check if pair quote currency equals to the stake currency. # Check if pair quote currency equals to the stake currency.
filtered_tickers = [v for k, v in tickers.items() filtered_tickers = [
if (self._exchange.get_pair_quote_currency(k) == base_currency v for k, v in tickers.items()
and v[key] is not None)] if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
and v[self._sort_key] is not None)]
else: 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] filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
if min_val > 0: if self._min_value > 0:
filtered_tickers = list(filter(lambda t: t[key] > min_val, filtered_tickers)) 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 # Validate whitelist to only have active market pairs
pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers]) pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers])
pairs = self._verify_blacklist(pairs, aswarning=False) 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] pairs = pairs[:self._number_pairs]
return pairs return pairs

View File

@ -1,10 +1,8 @@
""" """
Static List provider PairList manager class
"""
Provides lists as configured in config.json
"""
import logging import logging
from copy import deepcopy
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
@ -83,25 +81,39 @@ class PairListManager():
""" """
Run pairlist through all configured pairlists. Run pairlist through all configured pairlists.
""" """
# Tickers should be cached to avoid calling the exchange on each call.
pairlist = self._whitelist.copy()
# tickers should be cached to avoid calling the exchange on each call.
tickers: Dict = {} tickers: Dict = {}
if self._tickers_needed: if self._tickers_needed:
tickers = self._get_cached_tickers() 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 # Process all pairlists in chain
for pl in self._pairlists: for pl in self._pairlists:
pairlist = pl.filter_pairlist(pairlist, tickers) 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) pairlist = IPairList.verify_blacklist(pairlist, self.blacklist, True)
self._whitelist = pairlist 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: def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
""" """
Create list of pair tuples with (pair, ticker_interval) Create list of pair tuples with (pair, ticker_interval)
""" """
return [(pair, timeframe or self._config['ticker_interval']) for pair in pairs] return [(pair, timeframe or self._config['ticker_interval']) for pair in pairs]

View File

@ -86,7 +86,7 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}') logger.debug(f'trying {table_back_name}')
# Check for latest column # 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}') logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open = get_column_def(cols, 'fee_open', 'fee')
@ -113,6 +113,7 @@ def check_migrate(engine) -> None:
close_profit_abs = get_column_def( close_profit_abs = get_column_def(
cols, 'close_profit_abs', cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
# Schema migration necessary # Schema migration necessary
engine.execute(f"alter table trades rename to {table_back_name}") 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, stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update, 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 ticker_interval, open_trade_price, close_profit_abs
) )
select id, lower(exchange), select id, lower(exchange),
@ -153,6 +154,7 @@ def check_migrate(engine) -> None:
{initial_stop_loss_pct} initial_stop_loss_pct, {initial_stop_loss_pct} initial_stop_loss_pct,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{sell_order_status} sell_order_status,
{strategy} strategy, {ticker_interval} ticker_interval, {strategy} strategy, {ticker_interval} ticker_interval,
{open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
from {table_back_name} from {table_back_name}
@ -228,6 +230,7 @@ class Trade(_DECL_BASE):
# Lowest price reached # Lowest price reached
min_rate = Column(Float, nullable=True) min_rate = Column(Float, nullable=True)
sell_reason = Column(String, nullable=True) sell_reason = Column(String, nullable=True)
sell_order_status = Column(String, nullable=True)
strategy = Column(String, nullable=True) strategy = Column(String, nullable=True)
ticker_interval = Column(Integer, nullable=True) ticker_interval = Column(Integer, nullable=True)
@ -267,6 +270,7 @@ class Trade(_DECL_BASE):
'stake_amount': round(self.stake_amount, 8), 'stake_amount': round(self.stake_amount, 8),
'close_profit': self.close_profit, 'close_profit': self.close_profit,
'sell_reason': self.sell_reason, 'sell_reason': self.sell_reason,
'sell_order_status': self.sell_order_status,
'stop_loss': self.stop_loss, 'stop_loss': self.stop_loss,
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
'initial_stop_loss': self.initial_stop_loss, 'initial_stop_loss': self.initial_stop_loss,
@ -370,6 +374,7 @@ class Trade(_DECL_BASE):
self.close_profit_abs = self.calc_profit() self.close_profit_abs = self.calc_profit()
self.close_date = datetime.utcnow() self.close_date = datetime.utcnow()
self.is_open = False self.is_open = False
self.sell_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
logger.info( logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.', 'Marking %s as closed as the trade is fulfilled and found no open orders for it.',

View File

@ -186,7 +186,7 @@ class RPC:
def _rpc_daily_profit( def _rpc_daily_profit(
self, timescale: int, 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() today = datetime.utcnow().date()
profit_days: Dict[date, Dict] = {} profit_days: Dict[date, Dict] = {}
@ -206,28 +206,26 @@ class RPC:
'trades': len(trades) 'trades': len(trades)
} }
return [ data = [
[ {
key, 'date': key,
'{value:.8f} {symbol}'.format( 'abs_profit': f'{float(value["amount"]):.8f}',
value=float(value['amount']), 'fiat_value': '{value:.3f}'.format(
symbol=stake_currency
),
'{value:.3f} {symbol}'.format(
value=self._fiat_converter.convert_amount( value=self._fiat_converter.convert_amount(
value['amount'], value['amount'],
stake_currency, stake_currency,
fiat_display_currency fiat_display_currency
) if self._fiat_converter else 0, ) if self._fiat_converter else 0,
symbol=fiat_display_currency
), ),
'{value} trade{s}'.format( 'trade_count': f'{value["trades"]}',
value=value['trades'], }
s='' if value['trades'] < 2 else 's'
),
]
for key, value in profit_days.items() 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: def _rpc_trade_history(self, limit: int) -> Dict:
""" Returns the X last trades """ """ Returns the X last trades """
@ -547,5 +545,5 @@ class RPC:
def _rpc_edge(self) -> List[Dict[str, Any]]: def _rpc_edge(self) -> List[Dict[str, Any]]:
""" Returns information related to Edge """ """ Returns information related to Edge """
if not self._freqtrade.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() return self._freqtrade.edge.accepted_pairs()

View File

@ -226,11 +226,15 @@ class Telegram(RPC):
# Adding stoploss and stoploss percentage only if it is not None # Adding stoploss and stoploss percentage only if it is not None
"*Stoploss:* `{stop_loss:.8f}` " + "*Stoploss:* `{stop_loss:.8f}` " +
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), ("`({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 # 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: for msg in messages:
self._send_msg(msg) self._send_msg(msg)
@ -276,14 +280,18 @@ class Telegram(RPC):
stake_cur, stake_cur,
fiat_disp_cur fiat_disp_cur
) )
stats_tab = tabulate(stats, stats_tab = tabulate(
headers=[ [[day['date'],
'Day', f"{day['abs_profit']} {stats['stake_currency']}",
f'Profit {stake_cur}', f"{day['fiat_value']} {stats['fiat_display_currency']}",
f'Profit {fiat_disp_cur}', f"{day['trade_count']} trades"] for day in stats['data']],
f'Trades' headers=[
], 'Day',
tablefmt='simple') 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>' message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
self._send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:

View File

@ -47,9 +47,9 @@ class Webhook(RPC):
valuedict = self._config['webhook'].get('webhooksell', None) valuedict = self._config['webhook'].get('webhooksell', None)
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhooksellcancel', None) valuedict = self._config['webhook'].get('webhooksellcancel', None)
elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION, elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.CUSTOM_NOTIFICATION, RPCMessageType.CUSTOM_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION): RPCMessageType.WARNING_NOTIFICATION):
valuedict = self._config['webhook'].get('webhookstatus', None) valuedict = self._config['webhook'].get('webhookstatus', None)
else: else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) raise NotImplementedError('Unknown message type: {}'.format(msg['type']))

View File

@ -37,9 +37,7 @@ class Worker:
self._heartbeat_msg: float = 0 self._heartbeat_msg: float = 0
# Tell systemd that we completed initialization phase # Tell systemd that we completed initialization phase
if self._sd_notify: self._notify("READY=1")
logger.debug("sd_notify: READY=1")
self._sd_notify.notify("READY=1")
def _init(self, reconfig: bool) -> None: def _init(self, reconfig: bool) -> None:
""" """
@ -60,6 +58,15 @@ class Worker:
self._sd_notify = sdnotify.SystemdNotifier() if \ self._sd_notify = sdnotify.SystemdNotifier() if \
self._config.get('internals', {}).get('sd_notify', False) else None 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: def run(self) -> None:
state = None state = None
while True: while True:
@ -89,17 +96,13 @@ class Worker:
if state == State.STOPPED: if state == State.STOPPED:
# Ping systemd watchdog before sleeping in the stopped state # Ping systemd watchdog before sleeping in the stopped state
if self._sd_notify: self._notify("WATCHDOG=1\nSTATUS=State: STOPPED.")
logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.")
self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.")
self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs) self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs)
elif state == State.RUNNING: elif state == State.RUNNING:
# Ping systemd watchdog before throttling # Ping systemd watchdog before throttling
if self._sd_notify: self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.")
self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
self._throttle(func=self._process_running, throttle_secs=self._throttle_secs) self._throttle(func=self._process_running, throttle_secs=self._throttle_secs)
@ -154,9 +157,7 @@ class Worker:
replaces it with the new instance replaces it with the new instance
""" """
# Tell systemd that we initiated reconfiguration # Tell systemd that we initiated reconfiguration
if self._sd_notify: self._notify("RELOADING=1")
logger.debug("sd_notify: RELOADING=1")
self._sd_notify.notify("RELOADING=1")
# Clean up current freqtrade modules # Clean up current freqtrade modules
self.freqtrade.cleanup() self.freqtrade.cleanup()
@ -167,15 +168,11 @@ class Worker:
self.freqtrade.notify_status('config reloaded') self.freqtrade.notify_status('config reloaded')
# Tell systemd that we completed reconfiguration # Tell systemd that we completed reconfiguration
if self._sd_notify: self._notify("READY=1")
logger.debug("sd_notify: READY=1")
self._sd_notify.notify("READY=1")
def exit(self) -> None: def exit(self) -> None:
# Tell systemd that we are exiting now # Tell systemd that we are exiting now
if self._sd_notify: self._notify("STOPPING=1")
logger.debug("sd_notify: STOPPING=1")
self._sd_notify.notify("STOPPING=1")
if self.freqtrade: if self.freqtrade:
self.freqtrade.notify_status('process died') self.freqtrade.notify_status('process died')

View File

@ -1,7 +1,7 @@
# requirements without requirements installable via conda # requirements without requirements installable via conda
# mainly used for Raspberry pi installs # mainly used for Raspberry pi installs
ccxt==1.27.49 ccxt==1.27.91
SQLAlchemy==1.3.16 SQLAlchemy==1.3.17
python-telegram-bot==12.7 python-telegram-bot==12.7
arrow==0.15.6 arrow==0.15.6
cachetools==4.1.0 cachetools==4.1.0

View File

@ -4,7 +4,7 @@
-r requirements-hyperopt.txt -r requirements-hyperopt.txt
coveralls==2.0.0 coveralls==2.0.0
flake8==3.7.9 flake8==3.8.1
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==4.1.0 flake8-tidy-imports==4.1.0
mypy==0.770 mypy==0.770

View File

@ -1705,7 +1705,7 @@ def hyperopt_results():
{ {
'loss': 0.4366182531161519, 'loss': 0.4366182531161519,
'params_dict': { '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 '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_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 '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, 'loss': 20.0,
'params_dict': { '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 '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 'params_details': {
'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 '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
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # 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
'stoploss': {'stoploss': -0.338070047333259}}, '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_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 '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, 'total_profit': 6.185e-05,
@ -1767,8 +1768,9 @@ def hyperopt_results():
}, { }, {
'loss': 4.713497421432944, '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_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 'params_details': {
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 '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_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 '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, 'total_profit': -0.06339929,

View File

@ -517,9 +517,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf) Exchange(default_conf)
assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange." assert log_has("Pair XRP/BTC is restricted for some users on this exchange."
f"Please check if you are impacted by this restriction " "Please check if you are impacted by this restriction "
f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog) "on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):

View File

@ -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) 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 multi = 20
else: else:
multi = 18 multi = 18

View File

@ -820,7 +820,7 @@ def test_continue_hyperopt(mocker, default_conf, caplog):
Hyperopt(default_conf) Hyperopt(default_conf)
assert unlinkmock.call_count == 0 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: def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:

View File

@ -69,44 +69,44 @@ def test_log_on_refresh(mocker, static_pl_conf, markets, tickers):
def test_load_pairlist_noexist(mocker, markets, default_conf): 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)) 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, with pytest.raises(OperationalException,
match=r"Impossible to load Pairlist 'NonexistingPairList'. " match=r"Impossible to load Pairlist 'NonexistingPairList'. "
r"This class does not exist or contains Python code errors."): 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) default_conf, {}, 1)
def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): 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)) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
freqtradebot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
# List ordered by BaseVolume # List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC'] whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed # 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 # Ensure config dict hasn't been changed
assert (static_pl_conf['exchange']['pair_whitelist'] == 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): 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( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True), exchange_has=MagicMock(return_value=True),
markets=PropertyMock(return_value=markets), markets=PropertyMock(return_value=markets),
) )
freqtradebot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
# List ordered by BaseVolume # List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC'] whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed # Ensure all except those in whitelist are removed
assert set(whitelist) == set(freqtradebot.pairlists.whitelist) assert set(whitelist) == set(freqtrade.pairlists.whitelist)
assert static_pl_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf): 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, get_tickers=tickers,
exchange_has=MagicMock(return_value=True), 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 # Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', '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 # argument: use the whitelist dynamically by exchange-volume
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC'] 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', whitelist_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {} 'config': {}
@ -136,7 +136,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r'`number_assets` not specified. Please check your configuration ' match=r'`number_assets` not specified. Please check your configuration '
r'for "pairlist.config.number_assets"'): 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): 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', 'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True), 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)) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty))
# argument: use the whitelist dynamically by exchange-volume # argument: use the whitelist dynamically by exchange-volume
whitelist = [] whitelist = []
whitelist_conf['exchange']['pair_whitelist'] = [] whitelist_conf['exchange']['pair_whitelist'] = []
freqtradebot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
pairslist = whitelist_conf['exchange']['pair_whitelist'] pairslist = whitelist_conf['exchange']['pair_whitelist']
assert set(whitelist) == set(pairslist) assert set(whitelist) == set(pairslist)
@ -206,6 +206,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
pairlists, base_currency, whitelist_result, pairlists, base_currency, whitelist_result,
caplog) -> None: caplog) -> None:
whitelist_conf['pairlists'] = pairlists whitelist_conf['pairlists'] = pairlists
whitelist_conf['stake_currency'] = base_currency
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) 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), markets=PropertyMock(return_value=shitcoinmarkets),
) )
freqtrade.config['stake_currency'] = base_currency
freqtrade.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
whitelist = freqtrade.pairlists.whitelist whitelist = freqtrade.pairlists.whitelist
@ -312,18 +312,18 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
exchange_has=MagicMock(return_value=True), exchange_has=MagicMock(return_value=True),
get_tickers=tickers get_tickers=tickers
) )
bot = get_patched_freqtradebot(mocker, whitelist_conf) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
assert bot.pairlists._pairlists[0]._last_refresh == 0 assert freqtrade.pairlists._pairlists[0]._last_refresh == 0
assert tickers.call_count == 0 assert tickers.call_count == 0
bot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
assert tickers.call_count == 1 assert tickers.call_count == 1
assert bot.pairlists._pairlists[0]._last_refresh != 0 assert freqtrade.pairlists._pairlists[0]._last_refresh != 0
lrf = bot.pairlists._pairlists[0]._last_refresh lrf = freqtrade.pairlists._pairlists[0]._last_refresh
bot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
assert tickers.call_count == 1 assert tickers.call_count == 1
# Time should not be updated. # 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): def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):

View File

@ -60,6 +60,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_trade_price': ANY, 'open_trade_price': ANY,
'close_rate_requested': ANY, 'close_rate_requested': ANY,
'sell_reason': ANY, 'sell_reason': ANY,
'sell_order_status': ANY,
'min_rate': ANY, 'min_rate': ANY,
'max_rate': ANY, 'max_rate': ANY,
'strategy': ANY, 'strategy': ANY,
@ -82,7 +83,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
} == results[0] } == results[0]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', 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() results = rpc._rpc_trade_status()
assert isnan(results[0]['current_profit']) assert isnan(results[0]['current_profit'])
assert isnan(results[0]['current_rate']) 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, 'open_trade_price': ANY,
'close_rate_requested': ANY, 'close_rate_requested': ANY,
'sell_reason': ANY, 'sell_reason': ANY,
'sell_order_status': ANY,
'min_rate': ANY, 'min_rate': ANY,
'max_rate': ANY, 'max_rate': ANY,
'strategy': 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] assert '-0.41% (-0.06)' == result[0][3]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', 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') result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'instantly' == result[0][2] assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1] assert 'ETH/BTC' in result[0][1]
@ -203,16 +205,18 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
# Try valid data # Try valid data
update.message.text = '/daily 2' update.message.text = '/daily 2'
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency) days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert len(days) == 7 assert len(days['data']) == 7
for day in days: 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'] # [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
assert (day[1] == '0.00000000 BTC' or assert (day['abs_profit'] == '0.00000000' or
day[1] == '0.00006217 BTC') day['abs_profit'] == '0.00006217')
assert (day[2] == '0.000 USD' or assert (day['fiat_value'] == '0.000' or
day[2] == '0.767 USD') day['fiat_value'] == '0.767')
# ensure first day is current date # 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 # Try invalid data
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'): 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 # Test non-available pair
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', 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) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats['trade_count'] == 2 assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now' assert stats['first_trade_date'] == 'just now'

View File

@ -333,8 +333,10 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
) )
rc = client_get(client, f"{BASE_URI}/daily") rc = client_get(client, f"{BASE_URI}/daily")
assert_response(rc) assert_response(rc)
assert len(rc.json) == 7 assert len(rc.json['data']) == 7
assert rc.json[0][0] == str(datetime.utcnow().date()) 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): 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_rate_requested': 1.098e-05,
'open_trade_price': 0.0010025, 'open_trade_price': 0.0010025,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None,
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'ticker_interval': 5}] 'ticker_interval': 5}]
@ -626,6 +629,7 @@ def test_api_forcebuy(botclient, mocker, fee):
'open_rate_requested': None, 'open_rate_requested': None,
'open_trade_price': 0.2460546025, 'open_trade_price': 0.2460546025,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None,
'strategy': None, 'strategy': None,
'ticker_interval': None 'ticker_interval': None
} }

View File

@ -170,6 +170,7 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None:
'current_profit': -0.59, 'current_profit': -0.59,
'initial_stop_loss': 1.098e-05, 'initial_stop_loss': 1.098e-05,
'stop_loss': 1.099e-05, 'stop_loss': 1.099e-05,
'sell_order_status': None,
'initial_stop_loss_pct': -0.05, 'initial_stop_loss_pct': -0.05,
'stop_loss_pct': -0.01, 'stop_loss_pct': -0.01,
'open_order': '(limit buy rem=0.00000000)' 'open_order': '(limit buy rem=0.00000000)'

View File

@ -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('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
mocker.patch.object(Path, "read_text", MagicMock(return_value=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') load_config_file('somefile')

View File

@ -1976,6 +1976,10 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
Trade.session.add(open_trade) 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 # Return false - trade remains open
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
freqtrade.check_handle_timedout() 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 open_trade.is_open = False
Trade.session.add(open_trade) 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) freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
# Return false - No impact # 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 assert cancel_order_mock.call_count == 1
def test_handle_cancel_sell_limit(mocker, default_conf) -> None: def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
patch_RPCManager(mocker) send_msg_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', '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 = 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, order = {'remaining': 1,
'amount': 1, 'amount': 1,
'status': "open"} 'status': "open"}
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
assert freqtrade.handle_cancel_sell(trade, order, reason) assert freqtrade.handle_cancel_sell(trade, order, reason)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1
send_msg_mock.reset_mock()
order['amount'] = 2 order['amount'] = 2
assert (freqtrade.handle_cancel_sell(trade, order, reason) assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED']
== CANCEL_REASON['PARTIALLY_FILLED'])
# Assert cancel_order was not called (callcount remains unchanged) # Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order_mock.call_count == 1 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: 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) caplog.set_level(logging.DEBUG)
# Sell as trailing-stop is reached # Sell as trailing-stop is reached
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert log_has( assert log_has("ETH/BTC - HIT STOP: current price at 0.000012, stoploss is 0.000015, "
f"ETH/BTC - HIT STOP: current price at 0.000012, " "initial stoploss was at 0.000010, trade opened at 0.000011", caplog)
f"stoploss is 0.000015, "
f"initial stoploss was at 0.000010, trade opened at 0.000011", caplog)
assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value 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 # stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False 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("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 - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000138501 assert trade.stop_loss == 0.0000138501
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', 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 # stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
assert log_has(f"ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%", assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%", caplog)
caplog) assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000138501 assert trade.stop_loss == 0.0000138501
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', 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 # stop-loss should not be adjusted as offset is not reached yet
assert freqtrade.handle_trade(trade) is False 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 assert trade.stop_loss == 0.0000098910
# price rises above the offset (rises 12% when the offset is 5.5%) # 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 freqtrade.handle_trade(trade) is False
assert log_has(f"ETH/BTC - Using positive stoploss: 0.05 offset: 0.055 profit: 0.1218%", assert log_has("ETH/BTC - Using positive stoploss: 0.05 offset: 0.055 profit: 0.1218%", caplog)
caplog) assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000117705 assert trade.stop_loss == 0.0000117705

View File

@ -477,6 +477,7 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.close_rate_requested is None assert trade.close_rate_requested is None
assert trade.close_rate is not None assert trade.close_rate is not None
assert pytest.approx(trade.close_profit_abs) == trade.calc_profit() 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): def test_migrate_new(mocker, default_conf, fee, caplog):
@ -756,6 +757,7 @@ def test_to_json(default_conf, fee):
'stake_amount': 0.001, 'stake_amount': 0.001,
'close_profit': None, 'close_profit': None,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None,
'stop_loss': None, 'stop_loss': None,
'stop_loss_pct': None, 'stop_loss_pct': None,
'initial_stop_loss': None, 'initial_stop_loss': None,
@ -810,6 +812,7 @@ def test_to_json(default_conf, fee):
'open_rate_requested': None, 'open_rate_requested': None,
'open_trade_price': 12.33075, 'open_trade_price': 12.33075,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None,
'strategy': None, 'strategy': None,
'ticker_interval': None} 'ticker_interval': None}