Merge branch 'develop' into fix/3579
This commit is contained in:
@@ -73,6 +73,7 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
|
||||
"hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time",
|
||||
"hyperopt_list_min_avg_profit", "hyperopt_list_max_avg_profit",
|
||||
"hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit",
|
||||
"hyperopt_list_min_objective", "hyperopt_list_max_objective",
|
||||
"print_colorized", "print_json", "hyperopt_list_no_details",
|
||||
"export_csv"]
|
||||
|
||||
|
@@ -455,37 +455,49 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"hyperopt_list_min_avg_time": Arg(
|
||||
'--min-avg-time',
|
||||
help='Select epochs on above average time.',
|
||||
help='Select epochs above average time.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_max_avg_time": Arg(
|
||||
'--max-avg-time',
|
||||
help='Select epochs on under average time.',
|
||||
help='Select epochs below average time.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_min_avg_profit": Arg(
|
||||
'--min-avg-profit',
|
||||
help='Select epochs on above average profit.',
|
||||
help='Select epochs above average profit.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_max_avg_profit": Arg(
|
||||
'--max-avg-profit',
|
||||
help='Select epochs on below average profit.',
|
||||
help='Select epochs below average profit.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_min_total_profit": Arg(
|
||||
'--min-total-profit',
|
||||
help='Select epochs on above total profit.',
|
||||
help='Select epochs above total profit.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_max_total_profit": Arg(
|
||||
'--max-total-profit',
|
||||
help='Select epochs on below total profit.',
|
||||
help='Select epochs below total profit.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_min_objective": Arg(
|
||||
'--min-objective',
|
||||
help='Select epochs above objective.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_max_objective": Arg(
|
||||
'--max-objective',
|
||||
help='Select epochs below objective.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
|
@@ -35,7 +35,9 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None)
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
||||
}
|
||||
|
||||
results_file = (config['user_data_dir'] /
|
||||
@@ -45,7 +47,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
epochs = Hyperopt.load_previous_results(results_file)
|
||||
total_epochs = len(epochs)
|
||||
|
||||
epochs = _hyperopt_filter_epochs(epochs, filteroptions)
|
||||
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
||||
|
||||
if print_colorized:
|
||||
colorama_init(autoreset=True)
|
||||
@@ -92,14 +94,16 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None)
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None)
|
||||
}
|
||||
|
||||
# Previous evaluations
|
||||
epochs = Hyperopt.load_previous_results(results_file)
|
||||
total_epochs = len(epochs)
|
||||
|
||||
epochs = _hyperopt_filter_epochs(epochs, filteroptions)
|
||||
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
||||
filtered_epochs = len(epochs)
|
||||
|
||||
if n > filtered_epochs:
|
||||
@@ -119,7 +123,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
header_str="Epoch details")
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
||||
def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
||||
"""
|
||||
Filter our items from the list of hyperopt results
|
||||
"""
|
||||
@@ -127,6 +131,24 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
||||
epochs = [x for x in epochs if x['is_best']]
|
||||
if filteroptions['only_profitable']:
|
||||
epochs = [x for x in epochs if x['results_metrics']['profit'] > 0]
|
||||
|
||||
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
|
||||
|
||||
logger.info(f"{len(epochs)} " +
|
||||
("best " if filteroptions['only_best'] else "") +
|
||||
("profitable " if filteroptions['only_profitable'] else "") +
|
||||
"epochs found.")
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_trades'] > 0:
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
@@ -137,6 +159,11 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
||||
x for x in epochs
|
||||
if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades']
|
||||
]
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_avg_time'] is not None:
|
||||
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
||||
epochs = [
|
||||
@@ -149,6 +176,12 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
||||
x for x in epochs
|
||||
if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time']
|
||||
]
|
||||
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_avg_profit'] is not None:
|
||||
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
||||
epochs = [
|
||||
@@ -173,10 +206,18 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
||||
x for x in epochs
|
||||
if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit']
|
||||
]
|
||||
return epochs
|
||||
|
||||
logger.info(f"{len(epochs)} " +
|
||||
("best " if filteroptions['only_best'] else "") +
|
||||
("profitable " if filteroptions['only_profitable'] else "") +
|
||||
"epochs found.")
|
||||
|
||||
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_objective'] is not None:
|
||||
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
||||
|
||||
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
|
||||
if filteroptions['filter_max_objective'] is not None:
|
||||
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
||||
|
||||
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
|
||||
|
||||
return epochs
|
||||
|
@@ -334,6 +334,12 @@ class Configuration:
|
||||
self._args_to_config(config, argname='hyperopt_list_max_total_profit',
|
||||
logstring='Parameter --max-total-profit detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_min_objective',
|
||||
logstring='Parameter --min-objective detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_max_objective',
|
||||
logstring='Parameter --max-objective detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_no_details',
|
||||
logstring='Parameter --no-details detected: {}')
|
||||
|
||||
|
@@ -156,7 +156,9 @@ CONF_SCHEMA = {
|
||||
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss_on_exchange': {'type': 'boolean'},
|
||||
'stoploss_on_exchange_interval': {'type': 'number'}
|
||||
'stoploss_on_exchange_interval': {'type': 'number'},
|
||||
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
|
||||
'maximum': 1.0}
|
||||
},
|
||||
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||
},
|
||||
|
@@ -281,8 +281,8 @@ class Edge:
|
||||
#
|
||||
# Removing Pumps
|
||||
if self.edge_config.get('remove_pumps', False):
|
||||
results = results.groupby(['pair', 'stoploss']).apply(
|
||||
lambda x: x[x['profit_abs'] < 2 * x['profit_abs'].std() + x['profit_abs'].mean()])
|
||||
results = results[results['profit_abs'] < 2 * results['profit_abs'].std()
|
||||
+ results['profit_abs'].mean()]
|
||||
##########################################################################
|
||||
|
||||
# Removing trades having a duration more than X minutes (set in config)
|
||||
|
@@ -81,7 +81,7 @@ class Binance(Exchange):
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
|
@@ -107,12 +107,12 @@ def retrier_async(f):
|
||||
except TemporaryError as ex:
|
||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||
if count > 0:
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
if isinstance(ex, DDosProtection):
|
||||
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
||||
logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
await asyncio.sleep(backoff_delay)
|
||||
return await wrapper(*args, **kwargs)
|
||||
else:
|
||||
@@ -131,13 +131,13 @@ def retrier(_func=None, retries=API_RETRY_COUNT):
|
||||
except (TemporaryError, RetryableOrderError) as ex:
|
||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||
if count > 0:
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError):
|
||||
# increasing backoff
|
||||
backoff_delay = calculate_backoff(count + 1, retries)
|
||||
logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
time.sleep(backoff_delay)
|
||||
return wrapper(*args, **kwargs)
|
||||
else:
|
||||
|
@@ -187,6 +187,11 @@ class Exchange:
|
||||
def timeframes(self) -> List[str]:
|
||||
return list((self._api.timeframes or {}).keys())
|
||||
|
||||
@property
|
||||
def ohlcv_candle_limit(self) -> int:
|
||||
"""exchange ohlcv candle limit"""
|
||||
return int(self._ohlcv_candle_limit)
|
||||
|
||||
@property
|
||||
def markets(self) -> Dict:
|
||||
"""exchange ccxt markets"""
|
||||
@@ -253,8 +258,8 @@ class Exchange:
|
||||
api.urls['api'] = api.urls['test']
|
||||
logger.info("Enabled Sandbox API on %s", name)
|
||||
else:
|
||||
logger.warning(name, "No Sandbox URL in CCXT, exiting. "
|
||||
"Please check your config.json")
|
||||
logger.warning(
|
||||
f"No Sandbox URL in CCXT for {name}, exiting. Please check your config.json")
|
||||
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
||||
|
||||
def _load_async_markets(self, reload: bool = False) -> None:
|
||||
@@ -521,13 +526,13 @@ class Exchange:
|
||||
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise ExchangeError(
|
||||
f'Could not create {ordertype} {side} order on market {pair}.'
|
||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
@@ -995,7 +1000,7 @@ class Exchange:
|
||||
if self.is_cancel_order_result_suitable(corder):
|
||||
return corder
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not cancel order {order_id}.")
|
||||
logger.warning(f"Could not cancel order {order_id} for {pair}.")
|
||||
try:
|
||||
order = self.fetch_order(order_id, pair)
|
||||
except InvalidOrderException:
|
||||
@@ -1004,7 +1009,7 @@ class Exchange:
|
||||
|
||||
return order
|
||||
|
||||
@retrier
|
||||
@retrier(retries=5)
|
||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
@@ -1018,10 +1023,10 @@ class Exchange:
|
||||
return self._api.fetch_order(order_id, pair)
|
||||
except ccxt.OrderNotFound as e:
|
||||
raise RetryableOrderError(
|
||||
f'Order not found (id: {order_id}). Message: {e}') from e
|
||||
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
|
||||
f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
|
@@ -78,7 +78,7 @@ class Ftx(Exchange):
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
@retrier(retries=5)
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
|
@@ -89,7 +89,7 @@ class Kraken(Exchange):
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
|
@@ -600,6 +600,7 @@ class FreqtradeBot:
|
||||
Sends rpc notification when a buy occured.
|
||||
"""
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
@@ -623,6 +624,7 @@ class FreqtradeBot:
|
||||
current_rate = self.get_buy_rate(trade.pair, False)
|
||||
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
@@ -660,7 +662,7 @@ class FreqtradeBot:
|
||||
trades_closed += 1
|
||||
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to sell trade: %s', exception)
|
||||
logger.warning('Unable to sell trade %s: %s', trade.pair, exception)
|
||||
|
||||
# Updating wallets if any trade occured
|
||||
if trades_closed:
|
||||
@@ -827,10 +829,8 @@ class FreqtradeBot:
|
||||
return False
|
||||
|
||||
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||
if (not stoploss_order):
|
||||
|
||||
if not stoploss_order:
|
||||
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
||||
|
||||
stop_price = trade.open_rate * (1 + stoploss)
|
||||
|
||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
|
||||
@@ -978,6 +978,12 @@ class FreqtradeBot:
|
||||
reason = constants.CANCEL_REASON['TIMEOUT']
|
||||
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
||||
trade.amount)
|
||||
# Avoid race condition where the order could not be cancelled coz its already filled.
|
||||
# Simply bailing here is the only safe way - as this order will then be
|
||||
# handled in the next iteration.
|
||||
if corder.get('status') not in ('canceled', 'closed'):
|
||||
logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
|
||||
return False
|
||||
else:
|
||||
# Order was cancelled already, so we can reuse the existing dict
|
||||
corder = order
|
||||
@@ -1153,6 +1159,7 @@ class FreqtradeBot:
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'trade_id': trade.id,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
@@ -1195,6 +1202,7 @@ class FreqtradeBot:
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||
'trade_id': trade.id,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
|
@@ -101,7 +101,7 @@ class Backtesting:
|
||||
if len(self.pairlists.whitelist) == 0:
|
||||
raise OperationalException("No pair in whitelist.")
|
||||
|
||||
if config.get('fee'):
|
||||
if config.get('fee', None) is not None:
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||
|
@@ -5,6 +5,7 @@ import logging
|
||||
import arrow
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
@@ -23,6 +24,13 @@ class AgeFilter(IPairList):
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||
|
||||
if self._min_days_listed < 1:
|
||||
raise OperationalException("AgeFilter requires min_days_listed must be >= 1")
|
||||
if self._min_days_listed > exchange.ohlcv_candle_limit:
|
||||
raise OperationalException("AgeFilter requires min_days_listed must not exceed "
|
||||
"exchange max request size "
|
||||
f"({exchange.ohlcv_candle_limit})")
|
||||
self._enabled = self._min_days_listed >= 1
|
||||
|
||||
@property
|
||||
|
@@ -18,7 +18,11 @@ class PriceFilter(IPairList):
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0)
|
||||
self._enabled = self._low_price_ratio != 0
|
||||
self._min_price = pairlistconfig.get('min_price', 0)
|
||||
self._max_price = pairlistconfig.get('max_price', 0)
|
||||
self._enabled = ((self._low_price_ratio != 0) or
|
||||
(self._min_price != 0) or
|
||||
(self._max_price != 0))
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
@@ -33,7 +37,18 @@ class PriceFilter(IPairList):
|
||||
"""
|
||||
Short whitelist method description - used for startup-messages
|
||||
"""
|
||||
return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%."
|
||||
active_price_filters = []
|
||||
if self._low_price_ratio != 0:
|
||||
active_price_filters.append(f"below {self._low_price_ratio * 100}%")
|
||||
if self._min_price != 0:
|
||||
active_price_filters.append(f"below {self._min_price:.8f}")
|
||||
if self._max_price != 0:
|
||||
active_price_filters.append(f"above {self._max_price:.8f}")
|
||||
|
||||
if len(active_price_filters):
|
||||
return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}."
|
||||
|
||||
return f"{self.name} - No price filters configured."
|
||||
|
||||
def _validate_pair(self, ticker) -> bool:
|
||||
"""
|
||||
@@ -41,15 +56,33 @@ class PriceFilter(IPairList):
|
||||
:param ticker: ticker dict as returned from ccxt.load_markets()
|
||||
:return: True if the pair can stay, false if it should be removed
|
||||
"""
|
||||
if ticker['last'] is None:
|
||||
if ticker['last'] is None or ticker['last'] == 0:
|
||||
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 = 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}%")
|
||||
return False
|
||||
|
||||
# Perform low_price_ratio check.
|
||||
if self._low_price_ratio != 0:
|
||||
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}%")
|
||||
return False
|
||||
|
||||
# Perform min_price check.
|
||||
if self._min_price != 0:
|
||||
if ticker['last'] < self._min_price:
|
||||
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||
f"because last price < {self._min_price:.8f}")
|
||||
return False
|
||||
|
||||
# Perform max_price check.
|
||||
if self._max_price != 0:
|
||||
if ticker['last'] > self._max_price:
|
||||
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||
f"because last price > {self._max_price:.8f}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@@ -10,11 +10,13 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown,
|
||||
create_cum_profit,
|
||||
extract_trades_of_period, load_trades)
|
||||
from freqtrade.data.converter import trim_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.data.history import load_data
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_prev_date
|
||||
from freqtrade.misc import pair_to_filename
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy import IStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -467,6 +469,8 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
"""
|
||||
strategy = StrategyResolver.load_strategy(config)
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||
IStrategy.dp = DataProvider(config, exchange)
|
||||
plot_elements = init_plotscript(config)
|
||||
trades = plot_elements['trades']
|
||||
pair_counter = 0
|
||||
|
@@ -42,14 +42,14 @@ class HyperOptResolver(IResolver):
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
if not hasattr(hyperopt, 'populate_indicators'):
|
||||
logger.warning("Hyperopt class does not provide populate_indicators() method. "
|
||||
"Using populate_indicators from the strategy.")
|
||||
logger.info("Hyperopt class does not provide populate_indicators() method. "
|
||||
"Using populate_indicators from the strategy.")
|
||||
if not hasattr(hyperopt, 'populate_buy_trend'):
|
||||
logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
|
||||
"Using populate_buy_trend from the strategy.")
|
||||
logger.info("Hyperopt class does not provide populate_buy_trend() method. "
|
||||
"Using populate_buy_trend from the strategy.")
|
||||
if not hasattr(hyperopt, 'populate_sell_trend'):
|
||||
logger.warning("Hyperopt class does not provide populate_sell_trend() method. "
|
||||
"Using populate_sell_trend from the strategy.")
|
||||
logger.info("Hyperopt class does not provide populate_sell_trend() method. "
|
||||
"Using populate_sell_trend from the strategy.")
|
||||
return hyperopt
|
||||
|
||||
|
||||
|
@@ -56,7 +56,7 @@ def require_login(func: Callable[[Any, Any], Any]):
|
||||
|
||||
|
||||
# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency
|
||||
def rpc_catch_errors(func: Callable[[Any], Any]):
|
||||
def rpc_catch_errors(func: Callable[..., Any]):
|
||||
|
||||
def func_wrapper(obj, *args, **kwargs):
|
||||
|
||||
@@ -200,6 +200,8 @@ class ApiServer(RPC):
|
||||
view_func=self._ping, methods=['GET'])
|
||||
self.app.add_url_rule(f'{BASE_URI}/trades', 'trades',
|
||||
view_func=self._trades, methods=['GET'])
|
||||
self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete',
|
||||
view_func=self._trades_delete, methods=['DELETE'])
|
||||
# Combined actions and infos
|
||||
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
|
||||
methods=['GET', 'POST'])
|
||||
@@ -424,6 +426,19 @@ class ApiServer(RPC):
|
||||
results = self._rpc_trade_history(limit)
|
||||
return self.rest_dump(results)
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
def _trades_delete(self, tradeid):
|
||||
"""
|
||||
Handler for DELETE /trades/<tradeid> endpoint.
|
||||
Removes the trade from the database (tries to cancel open orders first!)
|
||||
get:
|
||||
param:
|
||||
tradeid: Numeric trade-id assigned to the trade.
|
||||
"""
|
||||
result = self._rpc_delete(tradeid)
|
||||
return self.rest_dump(result)
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
def _whitelist(self):
|
||||
|
@@ -6,14 +6,14 @@ from abc import abstractmethod
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from math import isnan
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import arrow
|
||||
from numpy import NAN, mean
|
||||
|
||||
from freqtrade.exceptions import ExchangeError, PricingError
|
||||
|
||||
from freqtrade.exchange import timeframe_to_msecs, timeframe_to_minutes
|
||||
from freqtrade.exceptions import (ExchangeError, InvalidOrderException,
|
||||
PricingError)
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||
from freqtrade.misc import shorten_date
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
@@ -252,9 +252,10 @@ class RPC:
|
||||
def _rpc_trade_history(self, limit: int) -> Dict:
|
||||
""" Returns the X last trades """
|
||||
if limit > 0:
|
||||
trades = Trade.get_trades().order_by(Trade.id.desc()).limit(limit)
|
||||
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
|
||||
Trade.id.desc()).limit(limit)
|
||||
else:
|
||||
trades = Trade.get_trades().order_by(Trade.id.desc()).all()
|
||||
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all()
|
||||
|
||||
output = [trade.to_json() for trade in trades]
|
||||
|
||||
@@ -523,7 +524,7 @@ class RPC:
|
||||
# check if valid pair
|
||||
|
||||
# check if pair already has an open pair
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
if trade:
|
||||
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
||||
|
||||
@@ -532,11 +533,51 @@ class RPC:
|
||||
|
||||
# execute buy
|
||||
if self._freqtrade.execute_buy(pair, stakeamount, price):
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
return trade
|
||||
else:
|
||||
return None
|
||||
|
||||
def _rpc_delete(self, trade_id: str) -> Dict[str, Union[str, int]]:
|
||||
"""
|
||||
Handler for delete <id>.
|
||||
Delete the given trade and close eventually existing open orders.
|
||||
"""
|
||||
with self._freqtrade._sell_lock:
|
||||
c_count = 0
|
||||
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
|
||||
if not trade:
|
||||
logger.warning('delete trade: Invalid argument received')
|
||||
raise RPCException('invalid argument')
|
||||
|
||||
# Try cancelling regular order if that exists
|
||||
if trade.open_order_id:
|
||||
try:
|
||||
self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
c_count += 1
|
||||
except (ExchangeError, InvalidOrderException):
|
||||
pass
|
||||
|
||||
# cancel stoploss on exchange ...
|
||||
if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
|
||||
and trade.stoploss_order_id):
|
||||
try:
|
||||
self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id,
|
||||
trade.pair)
|
||||
c_count += 1
|
||||
except (ExchangeError, InvalidOrderException):
|
||||
pass
|
||||
|
||||
Trade.session.delete(trade)
|
||||
Trade.session.flush()
|
||||
self._freqtrade.wallets.update()
|
||||
return {
|
||||
'result': 'success',
|
||||
'trade_id': trade_id,
|
||||
'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.',
|
||||
'cancel_order_count': c_count,
|
||||
}
|
||||
|
||||
def _rpc_performance(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for performance.
|
||||
|
@@ -5,6 +5,7 @@ This module manage Telegram communication
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import arrow
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from tabulate import tabulate
|
||||
@@ -92,6 +93,8 @@ class Telegram(RPC):
|
||||
CommandHandler('stop', self._stop),
|
||||
CommandHandler('forcesell', self._forcesell),
|
||||
CommandHandler('forcebuy', self._forcebuy),
|
||||
CommandHandler('trades', self._trades),
|
||||
CommandHandler('delete', self._delete_trade),
|
||||
CommandHandler('performance', self._performance),
|
||||
CommandHandler('daily', self._daily),
|
||||
CommandHandler('count', self._count),
|
||||
@@ -496,6 +499,62 @@ class Telegram(RPC):
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _trades(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /trades <n>
|
||||
Returns last n recent trades.
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
stake_cur = self._config['stake_currency']
|
||||
try:
|
||||
nrecent = int(context.args[0])
|
||||
except (TypeError, ValueError, IndexError):
|
||||
nrecent = 10
|
||||
try:
|
||||
trades = self._rpc_trade_history(
|
||||
nrecent
|
||||
)
|
||||
trades_tab = tabulate(
|
||||
[[arrow.get(trade['open_date']).humanize(),
|
||||
trade['pair'],
|
||||
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
|
||||
for trade in trades['trades']],
|
||||
headers=[
|
||||
'Open Date',
|
||||
'Pair',
|
||||
f'Profit ({stake_cur})',
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
|
||||
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _delete_trade(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /delete <id>.
|
||||
Delete the given trade
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
|
||||
trade_id = context.args[0] if len(context.args) > 0 else None
|
||||
try:
|
||||
msg = self._rpc_delete(trade_id)
|
||||
self._send_msg((
|
||||
'`{result_msg}`\n'
|
||||
'Please make sure to take care of this asset on the exchange manually.'
|
||||
).format(**msg))
|
||||
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
@@ -609,10 +668,12 @@ class Telegram(RPC):
|
||||
" *table :* `will display trades in a table`\n"
|
||||
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||
"*/profit:* `Lists cumulative profit from all finished trades`\n"
|
||||
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
||||
"regardless of profit`\n"
|
||||
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
|
||||
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
||||
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
||||
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
||||
"*/count:* `Show number of trades running compared to allowed number of trades`"
|
||||
|
@@ -34,7 +34,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
|
||||
"""
|
||||
return True
|
||||
|
||||
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||||
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
|
||||
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a regular sell order.
|
||||
|
Reference in New Issue
Block a user