Merge branch 'develop' into hyperopt-simplified-interface
This commit is contained in:
@@ -3,6 +3,7 @@ This module contains the argument manager class
|
||||
"""
|
||||
import argparse
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
|
||||
from freqtrade import constants
|
||||
@@ -47,12 +48,10 @@ class Arguments(object):
|
||||
"""
|
||||
Arguments Class. Manage the arguments received by the cli
|
||||
"""
|
||||
def __init__(self, args: Optional[List[str]], description: str,
|
||||
no_default_config: bool = False) -> None:
|
||||
def __init__(self, args: Optional[List[str]]) -> None:
|
||||
self.args = args
|
||||
self._parsed_arg: Optional[argparse.Namespace] = None
|
||||
self.parser = argparse.ArgumentParser(description=description)
|
||||
self._no_default_config = no_default_config
|
||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||
|
||||
def _load_args(self) -> None:
|
||||
self._build_args(optionlist=ARGS_MAIN)
|
||||
@@ -75,11 +74,13 @@ class Arguments(object):
|
||||
"""
|
||||
parsed_arg = self.parser.parse_args(self.args)
|
||||
|
||||
# When no config is provided, but a config exists, use that configuration!
|
||||
|
||||
# Workaround issue in argparse with action='append' and default value
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
# Allow no-config for certain commands (like downloading / plotting)
|
||||
if (not self._no_default_config and parsed_arg.config is None
|
||||
and not ('subparser' in parsed_arg and parsed_arg.subparser in NO_CONF_REQURIED)):
|
||||
if (parsed_arg.config is None and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or
|
||||
not ('subparser' in parsed_arg and parsed_arg.subparser in NO_CONF_REQURIED))):
|
||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
|
@@ -121,6 +121,7 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss_on_exchange': {'type': 'boolean'},
|
||||
'stoploss_on_exchange_interval': {'type': 'number'}
|
||||
|
@@ -4,7 +4,8 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade import DependencyException, OperationalException, TemporaryError
|
||||
from freqtrade import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -66,12 +67,14 @@ class Binance(Exchange):
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
f'Tried to sell amount {amount} at rate {rate}.'
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
# Errors:
|
||||
# `binance Order would trigger immediately.`
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}.'
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
|
@@ -611,6 +611,33 @@ class FreqtradeBot(object):
|
||||
logger.debug('Found no sell signal for %s.', trade)
|
||||
return False
|
||||
|
||||
def create_stoploss_order(self, trade: Trade, stop_price: float, rate: float) -> bool:
|
||||
"""
|
||||
Abstracts creating stoploss orders from the logic.
|
||||
Handles errors and updates the trade database object.
|
||||
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
|
||||
:return: True if the order succeeded, and False in case of problems.
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below price
|
||||
LIMIT_PRICE_PCT = 0.99
|
||||
|
||||
try:
|
||||
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
|
||||
stop_price=stop_price,
|
||||
rate=rate * LIMIT_PRICE_PCT)
|
||||
trade.stoploss_order_id = str(stoploss_order['id'])
|
||||
return True
|
||||
except InvalidOrderException as e:
|
||||
trade.stoploss_order_id = None
|
||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||
logger.warning('Selling the trade forcefully')
|
||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
|
||||
|
||||
except DependencyException:
|
||||
trade.stoploss_order_id = None
|
||||
logger.exception('Unable to place a stoploss order on exchange.')
|
||||
return False
|
||||
|
||||
def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
|
||||
"""
|
||||
Check if trade is fulfilled in which case the stoploss
|
||||
@@ -629,49 +656,25 @@ class FreqtradeBot(object):
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||
|
||||
# If trade open order id does not exist: buy order is fulfilled
|
||||
buy_order_fulfilled = not trade.open_order_id
|
||||
|
||||
# Limit price threshold: As limit price should always be below price
|
||||
limit_price_pct = 0.99
|
||||
|
||||
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||
if (buy_order_fulfilled and not stoploss_order):
|
||||
if self.edge:
|
||||
stoploss = self.edge.stoploss(pair=trade.pair)
|
||||
else:
|
||||
stoploss = self.strategy.stoploss
|
||||
if (not trade.open_order_id and not stoploss_order):
|
||||
|
||||
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
||||
|
||||
stop_price = trade.open_rate * (1 + stoploss)
|
||||
|
||||
# limit price should be less than stop price.
|
||||
limit_price = stop_price * limit_price_pct
|
||||
|
||||
try:
|
||||
stoploss_order_id = self.exchange.stoploss_limit(
|
||||
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
|
||||
)['id']
|
||||
trade.stoploss_order_id = str(stoploss_order_id)
|
||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
|
||||
trade.stoploss_last_update = datetime.now()
|
||||
return False
|
||||
|
||||
except DependencyException as exception:
|
||||
trade.stoploss_order_id = None
|
||||
logger.warning('Unable to place a stoploss order on exchange: %s', exception)
|
||||
|
||||
# If stoploss order is canceled for some reason we add it
|
||||
if stoploss_order and stoploss_order['status'] == 'canceled':
|
||||
try:
|
||||
stoploss_order_id = self.exchange.stoploss_limit(
|
||||
pair=trade.pair, amount=trade.amount,
|
||||
stop_price=trade.stop_loss, rate=trade.stop_loss * limit_price_pct
|
||||
)['id']
|
||||
trade.stoploss_order_id = str(stoploss_order_id)
|
||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||
rate=trade.stop_loss):
|
||||
return False
|
||||
except DependencyException as exception:
|
||||
else:
|
||||
trade.stoploss_order_id = None
|
||||
logger.warning('Stoploss order was cancelled, '
|
||||
'but unable to recreate one: %s', exception)
|
||||
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
|
||||
|
||||
# We check if stoploss order is fulfilled
|
||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
||||
@@ -680,7 +683,7 @@ class FreqtradeBot(object):
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair,
|
||||
timeframe_to_next_date(self.config['ticker_interval']))
|
||||
self._notify_sell(trade)
|
||||
self._notify_sell(trade, "stoploss")
|
||||
return True
|
||||
|
||||
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
||||
@@ -714,17 +717,13 @@ class FreqtradeBot(object):
|
||||
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
||||
f"for pair {trade.pair}")
|
||||
|
||||
try:
|
||||
# creating the new one
|
||||
stoploss_order_id = self.exchange.stoploss_limit(
|
||||
pair=trade.pair, amount=trade.amount,
|
||||
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
|
||||
)['id']
|
||||
trade.stoploss_order_id = str(stoploss_order_id)
|
||||
except DependencyException:
|
||||
trade.stoploss_order_id = None
|
||||
logger.exception(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
# Create new stoploss order
|
||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||
rate=trade.stop_loss):
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
||||
buy: bool, sell: bool) -> bool:
|
||||
@@ -877,9 +876,14 @@ class FreqtradeBot(object):
|
||||
except InvalidOrderException:
|
||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||
|
||||
ordertype = self.strategy.order_types[sell_type]
|
||||
if sell_reason == SellType.EMERGENCY_SELL:
|
||||
# Emergencysells (default to market!)
|
||||
ordertype = self.strategy.order_types.get("emergencysell", "market")
|
||||
|
||||
# Execute sell and update trade record
|
||||
order = self.exchange.sell(pair=str(trade.pair),
|
||||
ordertype=self.strategy.order_types[sell_type],
|
||||
ordertype=ordertype,
|
||||
amount=trade.amount, rate=limit,
|
||||
time_in_force=self.strategy.order_time_in_force['sell']
|
||||
)
|
||||
@@ -895,9 +899,9 @@ class FreqtradeBot(object):
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
|
||||
|
||||
self._notify_sell(trade)
|
||||
self._notify_sell(trade, ordertype)
|
||||
|
||||
def _notify_sell(self, trade: Trade):
|
||||
def _notify_sell(self, trade: Trade, order_type: str):
|
||||
"""
|
||||
Sends rpc notification when a sell occured.
|
||||
"""
|
||||
@@ -914,7 +918,7 @@ class FreqtradeBot(object):
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'limit': trade.close_rate_requested,
|
||||
'order_type': self.strategy.order_types['sell'],
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
'current_rate': current_rate,
|
||||
|
@@ -31,10 +31,7 @@ def main(sysargv: List[str] = None) -> None:
|
||||
return_code: Any = 1
|
||||
worker = None
|
||||
try:
|
||||
arguments = Arguments(
|
||||
sysargv,
|
||||
'Free, open source crypto trading bot'
|
||||
)
|
||||
arguments = Arguments(sysargv)
|
||||
args: Namespace = arguments.get_parsed_arg()
|
||||
|
||||
# A subcommand has been issued.
|
||||
|
@@ -114,3 +114,10 @@ def deep_merge_dicts(source, destination):
|
||||
destination[key] = value
|
||||
|
||||
return destination
|
||||
|
||||
|
||||
def round_dict(d, n):
|
||||
"""
|
||||
Rounds float values in the dict to n digits after the decimal point.
|
||||
"""
|
||||
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
|
||||
|
@@ -24,8 +24,10 @@ from skopt.space import Dimension
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.history import load_data, get_timeframe
|
||||
from freqtrade.misc import round_dict
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
# Import IHyperOptLoss to allow users import from this file
|
||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
|
||||
|
||||
@@ -178,9 +180,11 @@ class Hyperopt:
|
||||
indent=4)
|
||||
if self.has_space('roi'):
|
||||
print("ROI table:")
|
||||
pprint(self.custom_hyperopt.generate_roi_table(params), indent=4)
|
||||
# Round printed values to 5 digits after the decimal point
|
||||
pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4)
|
||||
if self.has_space('stoploss'):
|
||||
print(f"Stoploss: {params.get('stoploss')}")
|
||||
# Also round to 5 digits after the decimal point
|
||||
print(f"Stoploss: {round(params.get('stoploss'), 5)}")
|
||||
|
||||
def log_results(self, results) -> None:
|
||||
"""
|
||||
|
@@ -2,6 +2,8 @@
|
||||
IHyperOpt interface
|
||||
This module defines the interface to apply for hyperopts
|
||||
"""
|
||||
import logging
|
||||
import math
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Callable, List
|
||||
@@ -10,6 +12,11 @@ from pandas import DataFrame
|
||||
from skopt.space import Dimension, Integer, Real
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import round_dict
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_exception_message(method: str, space: str) -> str:
|
||||
@@ -22,11 +29,9 @@ def _format_exception_message(method: str, space: str) -> str:
|
||||
class IHyperOpt(ABC):
|
||||
"""
|
||||
Interface for freqtrade hyperopts
|
||||
Defines the mandatory structure must follow any custom strategies
|
||||
Defines the mandatory structure must follow any custom hyperopts
|
||||
|
||||
Attributes you can use:
|
||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||
stoploss -> float: optimal stoploss designed for the strategy
|
||||
Class attributes you can use:
|
||||
ticker_interval -> int: value of the ticker interval to use for the strategy
|
||||
"""
|
||||
ticker_interval: str
|
||||
@@ -84,6 +89,83 @@ class IHyperOpt(ABC):
|
||||
|
||||
return roi_table
|
||||
|
||||
@staticmethod
|
||||
def roi_space() -> List[Dimension]:
|
||||
"""
|
||||
Create a ROI space.
|
||||
|
||||
Defines values to search for each ROI steps.
|
||||
|
||||
This method implements adaptive roi hyperspace with varied
|
||||
ranges for parameters which automatically adapts to the
|
||||
ticker interval used.
|
||||
|
||||
It's used by Freqtrade by default, if no custom roi_space method is defined.
|
||||
"""
|
||||
|
||||
# Default scaling coefficients for the roi hyperspace. Can be changed
|
||||
# to adjust resulting ranges of the ROI tables.
|
||||
# Increase if you need wider ranges in the roi hyperspace, decrease if shorter
|
||||
# ranges are needed.
|
||||
roi_t_alpha = 1.0
|
||||
roi_p_alpha = 1.0
|
||||
|
||||
ticker_interval_mins = timeframe_to_minutes(IHyperOpt.ticker_interval)
|
||||
|
||||
# We define here limits for the ROI space parameters automagically adapted to the
|
||||
# ticker_interval used by the bot:
|
||||
#
|
||||
# * 'roi_t' (limits for the time intervals in the ROI tables) components
|
||||
# are scaled linearly.
|
||||
# * 'roi_p' (limits for the ROI value steps) components are scaled logarithmically.
|
||||
#
|
||||
# The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space()
|
||||
# method for the 5m ticker interval.
|
||||
roi_t_scale = ticker_interval_mins / 5
|
||||
roi_p_scale = math.log1p(ticker_interval_mins) / math.log1p(5)
|
||||
roi_limits = {
|
||||
'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t2_min': int(10 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t2_max': int(60 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t3_min': int(10 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t3_max': int(40 * roi_t_scale * roi_t_alpha),
|
||||
'roi_p1_min': 0.01 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p1_max': 0.04 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p2_min': 0.01 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p2_max': 0.07 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p3_min': 0.01 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p3_max': 0.20 * roi_p_scale * roi_p_alpha,
|
||||
}
|
||||
logger.debug(f"Using roi space limits: {roi_limits}")
|
||||
p = {
|
||||
'roi_t1': roi_limits['roi_t1_min'],
|
||||
'roi_t2': roi_limits['roi_t2_min'],
|
||||
'roi_t3': roi_limits['roi_t3_min'],
|
||||
'roi_p1': roi_limits['roi_p1_min'],
|
||||
'roi_p2': roi_limits['roi_p2_min'],
|
||||
'roi_p3': roi_limits['roi_p3_min'],
|
||||
}
|
||||
logger.info(f"Min roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}")
|
||||
p = {
|
||||
'roi_t1': roi_limits['roi_t1_max'],
|
||||
'roi_t2': roi_limits['roi_t2_max'],
|
||||
'roi_t3': roi_limits['roi_t3_max'],
|
||||
'roi_p1': roi_limits['roi_p1_max'],
|
||||
'roi_p2': roi_limits['roi_p2_max'],
|
||||
'roi_p3': roi_limits['roi_p3_max'],
|
||||
}
|
||||
logger.info(f"Max roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}")
|
||||
|
||||
return [
|
||||
Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'),
|
||||
Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'),
|
||||
Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'),
|
||||
Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'),
|
||||
Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'),
|
||||
Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def stoploss_space() -> List[Dimension]:
|
||||
"""
|
||||
@@ -96,19 +178,14 @@ class IHyperOpt(ABC):
|
||||
Real(-0.5, -0.02, name='stoploss'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def roi_space() -> List[Dimension]:
|
||||
"""
|
||||
Create a ROI space.
|
||||
# This is needed for proper unpickling the class attribute ticker_interval
|
||||
# which is set to the actual value by the resolver.
|
||||
# Why do I still need such shamanic mantras in modern python?
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
state['ticker_interval'] = self.ticker_interval
|
||||
return state
|
||||
|
||||
Defines values to search for each ROI steps.
|
||||
You may override it in your custom Hyperopt class.
|
||||
"""
|
||||
return [
|
||||
Integer(10, 120, name='roi_t1'),
|
||||
Integer(10, 60, name='roi_t2'),
|
||||
Integer(10, 40, name='roi_t3'),
|
||||
Real(0.01, 0.04, name='roi_p1'),
|
||||
Real(0.01, 0.07, name='roi_p2'),
|
||||
Real(0.01, 0.20, name='roi_p3'),
|
||||
]
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
IHyperOpt.ticker_interval = state['ticker_interval']
|
||||
|
@@ -1,15 +1,24 @@
|
||||
from argparse import Namespace
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.utils import setup_utils_configuration
|
||||
|
||||
|
||||
def validate_plot_args(args: Namespace):
|
||||
args_tmp = vars(args)
|
||||
if not args_tmp.get('datadir') and not args_tmp.get('config'):
|
||||
raise OperationalException(
|
||||
"You need to specify either `--datadir` or `--config` "
|
||||
"for plot-profit and plot-dataframe.")
|
||||
|
||||
|
||||
def start_plot_dataframe(args: Namespace) -> None:
|
||||
"""
|
||||
Entrypoint for dataframe plotting
|
||||
"""
|
||||
# Import here to avoid errors if plot-dependencies are not installed.
|
||||
from freqtrade.plot.plotting import analyse_and_plot_pairs
|
||||
validate_plot_args(args)
|
||||
config = setup_utils_configuration(args, RunMode.PLOT)
|
||||
|
||||
analyse_and_plot_pairs(config)
|
||||
@@ -21,6 +30,7 @@ def start_plot_profit(args: Namespace) -> None:
|
||||
"""
|
||||
# Import here to avoid errors if plot-dependencies are not installed.
|
||||
from freqtrade.plot.plotting import plot_profit
|
||||
validate_plot_args(args)
|
||||
config = setup_utils_configuration(args, RunMode.PLOT)
|
||||
|
||||
plot_profit(config)
|
||||
|
@@ -35,7 +35,7 @@ class HyperOptResolver(IResolver):
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
# Assign ticker_interval to be used in hyperopt
|
||||
self.hyperopt.__class__.ticker_interval = str(config['ticker_interval'])
|
||||
IHyperOpt.ticker_interval = str(config['ticker_interval'])
|
||||
|
||||
if not hasattr(self.hyperopt, 'populate_buy_trend'):
|
||||
logger.warning("Custom Hyperopt does not provide populate_buy_trend. "
|
||||
|
@@ -4,12 +4,12 @@
|
||||
This module manage Telegram communication
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, List
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from tabulate import tabulate
|
||||
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
|
||||
from telegram import ParseMode, ReplyKeyboardMarkup, Update
|
||||
from telegram.error import NetworkError, TelegramError
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
from telegram.ext import CommandHandler, Updater, CallbackContext
|
||||
|
||||
from freqtrade.__init__ import __version__
|
||||
from freqtrade.rpc import RPC, RPCException, RPCMessageType
|
||||
@@ -31,7 +31,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
||||
"""
|
||||
def wrapper(self, *args, **kwargs):
|
||||
""" Decorator logic """
|
||||
update = kwargs.get('update') or args[1]
|
||||
update = kwargs.get('update') or args[0]
|
||||
|
||||
# Reject unauthorized messages
|
||||
chat_id = int(self._config['telegram']['chat_id'])
|
||||
@@ -79,7 +79,8 @@ class Telegram(RPC):
|
||||
registers all known command handlers
|
||||
and starts polling for message updates
|
||||
"""
|
||||
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
|
||||
self._updater = Updater(token=self._config['telegram']['token'], workers=0,
|
||||
use_context=True)
|
||||
|
||||
# Register command handler and start telegram message polling
|
||||
handles = [
|
||||
@@ -96,7 +97,7 @@ class Telegram(RPC):
|
||||
CommandHandler('reload_conf', self._reload_conf),
|
||||
CommandHandler('stopbuy', self._stopbuy),
|
||||
CommandHandler('whitelist', self._whitelist),
|
||||
CommandHandler('blacklist', self._blacklist, pass_args=True),
|
||||
CommandHandler('blacklist', self._blacklist),
|
||||
CommandHandler('edge', self._edge),
|
||||
CommandHandler('help', self._help),
|
||||
CommandHandler('version', self._version),
|
||||
@@ -175,7 +176,7 @@ class Telegram(RPC):
|
||||
self._send_msg(message)
|
||||
|
||||
@authorized_only
|
||||
def _status(self, bot: Bot, update: Update) -> None:
|
||||
def _status(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /status.
|
||||
Returns the current TradeThread status
|
||||
@@ -184,11 +185,8 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# Check if additional parameters are passed
|
||||
params = update.message.text.replace('/status', '').split(' ') \
|
||||
if update.message.text else []
|
||||
if 'table' in params:
|
||||
self._status_table(bot, update)
|
||||
if 'table' in context.args:
|
||||
self._status_table(update, context)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -221,13 +219,13 @@ class Telegram(RPC):
|
||||
messages.append("\n".join([l for l in lines if l]).format(**r))
|
||||
|
||||
for msg in messages:
|
||||
self._send_msg(msg, bot=bot)
|
||||
self._send_msg(msg)
|
||||
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _status_table(self, bot: Bot, update: Update) -> None:
|
||||
def _status_table(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /status table.
|
||||
Returns the current TradeThread status in table format
|
||||
@@ -240,10 +238,10 @@ class Telegram(RPC):
|
||||
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
||||
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _daily(self, bot: Bot, update: Update) -> None:
|
||||
def _daily(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /daily <n>
|
||||
Returns a daily profit (in BTC) over the last n days.
|
||||
@@ -254,8 +252,8 @@ class Telegram(RPC):
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
timescale = int(update.message.text.replace('/daily', '').strip())
|
||||
except (TypeError, ValueError):
|
||||
timescale = int(context.args[0])
|
||||
except (TypeError, ValueError, IndexError):
|
||||
timescale = 7
|
||||
try:
|
||||
stats = self._rpc_daily_profit(
|
||||
@@ -272,12 +270,12 @@ class Telegram(RPC):
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
||||
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _profit(self, bot: Bot, update: Update) -> None:
|
||||
def _profit(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /profit.
|
||||
Returns a cumulative profit statistics.
|
||||
@@ -317,12 +315,12 @@ class Telegram(RPC):
|
||||
f"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
||||
f"*Avg. Duration:* `{avg_duration}`\n" \
|
||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
|
||||
self._send_msg(markdown_msg, bot=bot)
|
||||
self._send_msg(markdown_msg)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _balance(self, bot: Bot, update: Update) -> None:
|
||||
def _balance(self, update: Update, context: CallbackContext) -> None:
|
||||
""" Handler for /balance """
|
||||
try:
|
||||
result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
|
||||
@@ -339,7 +337,7 @@ class Telegram(RPC):
|
||||
|
||||
# Handle overflowing messsage length
|
||||
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
self._send_msg(output, bot=bot)
|
||||
self._send_msg(output)
|
||||
output = curr_output
|
||||
else:
|
||||
output += curr_output
|
||||
@@ -347,12 +345,12 @@ class Telegram(RPC):
|
||||
output += "\n*Estimated Value*:\n" \
|
||||
"\t`BTC: {total: .8f}`\n" \
|
||||
"\t`{symbol}: {value: .2f}`\n".format(**result)
|
||||
self._send_msg(output, bot=bot)
|
||||
self._send_msg(output)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _start(self, bot: Bot, update: Update) -> None:
|
||||
def _start(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /start.
|
||||
Starts TradeThread
|
||||
@@ -361,10 +359,10 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_start()
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg))
|
||||
|
||||
@authorized_only
|
||||
def _stop(self, bot: Bot, update: Update) -> None:
|
||||
def _stop(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /stop.
|
||||
Stops TradeThread
|
||||
@@ -373,10 +371,10 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_stop()
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg))
|
||||
|
||||
@authorized_only
|
||||
def _reload_conf(self, bot: Bot, update: Update) -> None:
|
||||
def _reload_conf(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /reload_conf.
|
||||
Triggers a config file reload
|
||||
@@ -385,10 +383,10 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_reload_conf()
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg))
|
||||
|
||||
@authorized_only
|
||||
def _stopbuy(self, bot: Bot, update: Update) -> None:
|
||||
def _stopbuy(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /stop_buy.
|
||||
Sets max_open_trades to 0 and gracefully sells all open trades
|
||||
@@ -397,10 +395,10 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_stopbuy()
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg))
|
||||
|
||||
@authorized_only
|
||||
def _forcesell(self, bot: Bot, update: Update) -> None:
|
||||
def _forcesell(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /forcesell <id>.
|
||||
Sells the given trade at current price
|
||||
@@ -409,16 +407,16 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
trade_id = update.message.text.replace('/forcesell', '').strip()
|
||||
trade_id = context.args[0] if len(context.args) > 0 else None
|
||||
try:
|
||||
msg = self._rpc_forcesell(trade_id)
|
||||
self._send_msg('Forcesell Result: `{result}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Forcesell Result: `{result}`'.format(**msg))
|
||||
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _forcebuy(self, bot: Bot, update: Update) -> None:
|
||||
def _forcebuy(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /forcebuy <asset> <price>.
|
||||
Buys a pair trade at the given or current price
|
||||
@@ -427,16 +425,15 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
message = update.message.text.replace('/forcebuy', '').strip().split()
|
||||
pair = message[0]
|
||||
price = float(message[1]) if len(message) > 1 else None
|
||||
pair = context.args[0]
|
||||
price = float(context.args[1]) if len(context.args) > 1 else None
|
||||
try:
|
||||
self._rpc_forcebuy(pair, price)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _performance(self, bot: Bot, update: Update) -> None:
|
||||
def _performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /performance.
|
||||
Shows a performance statistic from finished trades
|
||||
@@ -455,10 +452,10 @@ class Telegram(RPC):
|
||||
message = '<b>Performance:</b>\n{}'.format(stats)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _count(self, bot: Bot, update: Update) -> None:
|
||||
def _count(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /count.
|
||||
Returns the number of trades running
|
||||
@@ -475,10 +472,10 @@ class Telegram(RPC):
|
||||
logger.debug(message)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _whitelist(self, bot: Bot, update: Update) -> None:
|
||||
def _whitelist(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /whitelist
|
||||
Shows the currently active whitelist
|
||||
@@ -492,17 +489,17 @@ class Telegram(RPC):
|
||||
logger.debug(message)
|
||||
self._send_msg(message)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _blacklist(self, bot: Bot, update: Update, args: List[str]) -> None:
|
||||
def _blacklist(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /blacklist
|
||||
Shows the currently active blacklist
|
||||
"""
|
||||
try:
|
||||
|
||||
blacklist = self._rpc_blacklist(args)
|
||||
blacklist = self._rpc_blacklist(context.args)
|
||||
|
||||
message = f"Blacklist contains {blacklist['length']} pairs\n"
|
||||
message += f"`{', '.join(blacklist['blacklist'])}`"
|
||||
@@ -510,10 +507,10 @@ class Telegram(RPC):
|
||||
logger.debug(message)
|
||||
self._send_msg(message)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _edge(self, bot: Bot, update: Update) -> None:
|
||||
def _edge(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /edge
|
||||
Shows information related to Edge
|
||||
@@ -522,12 +519,12 @@ class Telegram(RPC):
|
||||
edge_pairs = self._rpc_edge()
|
||||
edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple')
|
||||
message = f'<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>'
|
||||
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _help(self, bot: Bot, update: Update) -> None:
|
||||
def _help(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /help.
|
||||
Show commands of the bot
|
||||
@@ -559,10 +556,10 @@ class Telegram(RPC):
|
||||
"*/help:* `This help message`\n" \
|
||||
"*/version:* `Show version`"
|
||||
|
||||
self._send_msg(message, bot=bot)
|
||||
self._send_msg(message)
|
||||
|
||||
@authorized_only
|
||||
def _version(self, bot: Bot, update: Update) -> None:
|
||||
def _version(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /version.
|
||||
Show version information
|
||||
@@ -570,10 +567,9 @@ class Telegram(RPC):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
||||
self._send_msg('*Version:* `{}`'.format(__version__))
|
||||
|
||||
def _send_msg(self, msg: str, bot: Bot = None,
|
||||
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||
"""
|
||||
Send given markdown message
|
||||
:param msg: message
|
||||
@@ -581,7 +577,6 @@ class Telegram(RPC):
|
||||
:param parse_mode: telegram parse mode
|
||||
:return: None
|
||||
"""
|
||||
bot = bot or self._updater.bot
|
||||
|
||||
keyboard = [['/daily', '/profit', '/balance'],
|
||||
['/status', '/status table', '/performance'],
|
||||
@@ -591,7 +586,7 @@ class Telegram(RPC):
|
||||
|
||||
try:
|
||||
try:
|
||||
bot.send_message(
|
||||
self._updater.bot.send_message(
|
||||
self._config['telegram']['chat_id'],
|
||||
text=msg,
|
||||
parse_mode=parse_mode,
|
||||
@@ -604,7 +599,7 @@ class Telegram(RPC):
|
||||
'Telegram NetworkError: %s! Trying one more time.',
|
||||
network_err.message
|
||||
)
|
||||
bot.send_message(
|
||||
self._updater.bot.send_message(
|
||||
self._config['telegram']['chat_id'],
|
||||
text=msg,
|
||||
parse_mode=parse_mode,
|
||||
|
@@ -39,6 +39,7 @@ class SellType(Enum):
|
||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||
SELL_SIGNAL = "sell_signal"
|
||||
FORCE_SELL = "force_sell"
|
||||
EMERGENCY_SELL = "emergency_sell"
|
||||
NONE = ""
|
||||
|
||||
|
||||
|
@@ -45,7 +45,7 @@ def log_has_re(line, logs):
|
||||
|
||||
|
||||
def get_args(args):
|
||||
return Arguments(args, '').get_parsed_arg()
|
||||
return Arguments(args).get_parsed_arg()
|
||||
|
||||
|
||||
def patched_configuration_load_config_file(mocker, config) -> None:
|
||||
|
@@ -4,7 +4,8 @@ from unittest.mock import MagicMock
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade import DependencyException, OperationalException, TemporaryError
|
||||
from freqtrade import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
@@ -49,8 +50,9 @@ def test_stoploss_limit_order(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||
with pytest.raises(InvalidOrderException):
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
|
||||
|
@@ -418,7 +418,8 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
|
||||
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
|
||||
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result',
|
||||
'params': {'buy': {}, 'sell': {}, 'roi': {}, 'stoploss': 0.0}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
|
@@ -100,7 +100,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
||||
bot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(bot, (True, False))
|
||||
dummy = DummyCls(bot)
|
||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||
dummy.dummy_handler(update=update, context=MagicMock())
|
||||
assert dummy.state['called'] is True
|
||||
assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
|
||||
assert not log_has('Rejected unauthorized message from: 0', caplog)
|
||||
@@ -117,7 +117,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
||||
bot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(bot, (True, False))
|
||||
dummy = DummyCls(bot)
|
||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||
dummy.dummy_handler(update=update, context=MagicMock())
|
||||
assert dummy.state['called'] is False
|
||||
assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog)
|
||||
assert log_has('Rejected unauthorized message from: 3735928559', caplog)
|
||||
@@ -136,7 +136,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
||||
patch_get_signal(bot, (True, False))
|
||||
dummy = DummyCls(bot)
|
||||
|
||||
dummy.dummy_exception(bot=MagicMock(), update=update)
|
||||
dummy.dummy_exception(update=update, context=MagicMock())
|
||||
assert dummy.state['called'] is False
|
||||
assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
|
||||
assert not log_has('Rejected unauthorized message from: 0', caplog)
|
||||
@@ -194,12 +194,13 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||
for _ in range(3):
|
||||
freqtradebot.create_trades()
|
||||
|
||||
telegram._status(bot=MagicMock(), update=update)
|
||||
telegram._status(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
|
||||
update.message.text = MagicMock()
|
||||
update.message.text.replace = MagicMock(return_value='table 2 3')
|
||||
telegram._status(bot=MagicMock(), update=update)
|
||||
context = MagicMock()
|
||||
# /status table 2 3
|
||||
context.args = ["table", "2", "3"]
|
||||
telegram._status(update=update, context=context)
|
||||
assert status_table.call_count == 1
|
||||
|
||||
|
||||
@@ -228,13 +229,13 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
# Status is also enabled when stopped
|
||||
telegram._status(bot=MagicMock(), update=update)
|
||||
telegram._status(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
telegram._status(bot=MagicMock(), update=update)
|
||||
telegram._status(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
@@ -242,7 +243,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
|
||||
# Create some test data
|
||||
freqtradebot.create_trades()
|
||||
# Trigger status while we have a fulfilled order for the open trade
|
||||
telegram._status(bot=MagicMock(), update=update)
|
||||
telegram._status(update=update, context=MagicMock())
|
||||
|
||||
# close_rate should not be included in the message as the trade is not closed
|
||||
# and no line should be empty
|
||||
@@ -280,13 +281,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
# Status table is also enabled when stopped
|
||||
telegram._status_table(bot=MagicMock(), update=update)
|
||||
telegram._status_table(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no active order' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
telegram._status_table(bot=MagicMock(), update=update)
|
||||
telegram._status_table(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no active order' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
@@ -294,7 +295,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
||||
# Create some test data
|
||||
freqtradebot.create_trades()
|
||||
|
||||
telegram._status_table(bot=MagicMock(), update=update)
|
||||
telegram._status_table(update=update, context=MagicMock())
|
||||
|
||||
text = re.sub('</?pre>', '', msg_mock.call_args_list[-1][0][0])
|
||||
line = text.split("\n")
|
||||
@@ -346,8 +347,10 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
trade.is_open = False
|
||||
|
||||
# Try valid data
|
||||
update.message.text = '/daily 2'
|
||||
telegram._daily(bot=MagicMock(), update=update)
|
||||
# /daily 2
|
||||
context = MagicMock()
|
||||
context.args = ["2"]
|
||||
telegram._daily(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Daily' in msg_mock.call_args_list[0][0][0]
|
||||
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
||||
@@ -369,9 +372,10 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
update.message.text = '/daily 1'
|
||||
|
||||
telegram._daily(bot=MagicMock(), update=update)
|
||||
# /daily 1
|
||||
context = MagicMock()
|
||||
context.args = ["1"]
|
||||
telegram._daily(update=update, context=context)
|
||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
|
||||
@@ -398,16 +402,20 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
# Try invalid data
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.state = State.RUNNING
|
||||
update.message.text = '/daily -2'
|
||||
telegram._daily(bot=MagicMock(), update=update)
|
||||
# /daily -2
|
||||
context = MagicMock()
|
||||
context.args = ["-2"]
|
||||
telegram._daily(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Try invalid data
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.state = State.RUNNING
|
||||
update.message.text = '/daily today'
|
||||
telegram._daily(bot=MagicMock(), update=update)
|
||||
# /daily today
|
||||
context = MagicMock()
|
||||
context.args = ["today"]
|
||||
telegram._daily(update=update, context=context)
|
||||
assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
@@ -433,7 +441,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._profit(bot=MagicMock(), update=update)
|
||||
telegram._profit(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
@@ -445,7 +453,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
telegram._profit(bot=MagicMock(), update=update)
|
||||
telegram._profit(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||
msg_mock.reset_mock()
|
||||
@@ -457,7 +465,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
telegram._profit(bot=MagicMock(), update=update)
|
||||
telegram._profit(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0]
|
||||
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
|
||||
@@ -507,7 +515,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> N
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._balance(bot=MagicMock(), update=update)
|
||||
telegram._balance(update=update, context=MagicMock())
|
||||
result = msg_mock.call_args_list[0][0][0]
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*BTC:*' in result
|
||||
@@ -536,7 +544,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.config['dry_run'] = False
|
||||
telegram._balance(bot=MagicMock(), update=update)
|
||||
telegram._balance(update=update, context=MagicMock())
|
||||
result = msg_mock.call_args_list[0][0][0]
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'All balances are zero.' in result
|
||||
@@ -557,7 +565,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._balance(bot=MagicMock(), update=update)
|
||||
telegram._balance(update=update, context=MagicMock())
|
||||
result = msg_mock.call_args_list[0][0][0]
|
||||
assert msg_mock.call_count == 1
|
||||
assert "Running in Dry Run, balances are not available." in result
|
||||
@@ -593,7 +601,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._balance(bot=MagicMock(), update=update)
|
||||
telegram._balance(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count > 1
|
||||
# Test if wrap happens around 4000 -
|
||||
# and each single currency-output is around 120 characters long so we need
|
||||
@@ -615,7 +623,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
telegram._start(bot=MagicMock(), update=update)
|
||||
telegram._start(update=update, context=MagicMock())
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
assert msg_mock.call_count == 1
|
||||
|
||||
@@ -633,7 +641,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
telegram._start(bot=MagicMock(), update=update)
|
||||
telegram._start(update=update, context=MagicMock())
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'already running' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -652,7 +660,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
telegram._stop(bot=MagicMock(), update=update)
|
||||
telegram._stop(update=update, context=MagicMock())
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'stopping trader' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -671,7 +679,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
telegram._stop(bot=MagicMock(), update=update)
|
||||
telegram._stop(update=update, context=MagicMock())
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -689,7 +697,7 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None:
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
assert freqtradebot.config['max_open_trades'] != 0
|
||||
telegram._stopbuy(bot=MagicMock(), update=update)
|
||||
telegram._stopbuy(update=update, context=MagicMock())
|
||||
assert freqtradebot.config['max_open_trades'] == 0
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'No more buy will occur from now. Run /reload_conf to reset.' \
|
||||
@@ -709,7 +717,7 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
telegram._reload_conf(bot=MagicMock(), update=update)
|
||||
telegram._reload_conf(update=update, context=MagicMock())
|
||||
assert freqtradebot.state == State.RELOAD_CONF
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'reloading config' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -742,8 +750,10 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
|
||||
# Increase the price and sell it
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up)
|
||||
|
||||
update.message.text = '/forcesell 1'
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
# /forcesell 1
|
||||
context = MagicMock()
|
||||
context.args = ["1"]
|
||||
telegram._forcesell(update=update, context=context)
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||
@@ -796,8 +806,10 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
update.message.text = '/forcesell 1'
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
# /forcesell 1
|
||||
context = MagicMock()
|
||||
context.args = ["1"]
|
||||
telegram._forcesell(update=update, context=context)
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
|
||||
@@ -842,8 +854,10 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
|
||||
freqtradebot.create_trades()
|
||||
rpc_mock.reset_mock()
|
||||
|
||||
update.message.text = '/forcesell all'
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
# /forcesell all
|
||||
context = MagicMock()
|
||||
context.args = ["all"]
|
||||
telegram._forcesell(update=update, context=context)
|
||||
|
||||
assert rpc_mock.call_count == 4
|
||||
msg = rpc_mock.call_args_list[0][0][0]
|
||||
@@ -882,24 +896,29 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||
|
||||
# Trader is not running
|
||||
freqtradebot.state = State.STOPPED
|
||||
update.message.text = '/forcesell 1'
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
# /forcesell 1
|
||||
context = MagicMock()
|
||||
context.args = ["1"]
|
||||
telegram._forcesell(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# No argument
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.state = State.RUNNING
|
||||
update.message.text = '/forcesell'
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
context = MagicMock()
|
||||
context.args = []
|
||||
telegram._forcesell(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Invalid argument
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.state = State.RUNNING
|
||||
update.message.text = '/forcesell 123456'
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
# /forcesell 123456
|
||||
context = MagicMock()
|
||||
context.args = ["123456"]
|
||||
telegram._forcesell(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
@@ -921,8 +940,10 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None:
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
update.message.text = '/forcebuy ETH/BTC'
|
||||
telegram._forcebuy(bot=MagicMock(), update=update)
|
||||
# /forcebuy ETH/BTC
|
||||
context = MagicMock()
|
||||
context.args = ["ETH/BTC"]
|
||||
telegram._forcebuy(update=update, context=context)
|
||||
|
||||
assert fbuy_mock.call_count == 1
|
||||
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
|
||||
@@ -931,8 +952,10 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None:
|
||||
# Reset and retry with specified price
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
|
||||
update.message.text = '/forcebuy ETH/BTC 0.055'
|
||||
telegram._forcebuy(bot=MagicMock(), update=update)
|
||||
# /forcebuy ETH/BTC 0.055
|
||||
context = MagicMock()
|
||||
context.args = ["ETH/BTC", "0.055"]
|
||||
telegram._forcebuy(update=update, context=context)
|
||||
|
||||
assert fbuy_mock.call_count == 1
|
||||
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
|
||||
@@ -955,7 +978,7 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
update.message.text = '/forcebuy ETH/Nonepair'
|
||||
telegram._forcebuy(bot=MagicMock(), update=update)
|
||||
telegram._forcebuy(update=update, context=MagicMock())
|
||||
|
||||
assert rpc_mock.call_count == 1
|
||||
assert rpc_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.'
|
||||
@@ -995,7 +1018,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
telegram._performance(bot=MagicMock(), update=update)
|
||||
telegram._performance(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
||||
assert '<code>ETH/BTC\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -1021,7 +1044,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
telegram._count(bot=MagicMock(), update=update)
|
||||
telegram._count(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
@@ -1030,7 +1053,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
|
||||
# Create some test data
|
||||
freqtradebot.create_trades()
|
||||
msg_mock.reset_mock()
|
||||
telegram._count(bot=MagicMock(), update=update)
|
||||
telegram._count(update=update, context=MagicMock())
|
||||
|
||||
msg = '<pre> current max total stake\n--------- ----- -------------\n' \
|
||||
' 1 {} {}</pre>'\
|
||||
@@ -1052,7 +1075,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None:
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._whitelist(bot=MagicMock(), update=update)
|
||||
telegram._whitelist(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert ('Using whitelist `StaticPairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`'
|
||||
in msg_mock.call_args_list[0][0][0])
|
||||
@@ -1073,7 +1096,7 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None:
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._whitelist(bot=MagicMock(), update=update)
|
||||
telegram._whitelist(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert ('Using whitelist `VolumePairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`'
|
||||
in msg_mock.call_args_list[0][0][0])
|
||||
@@ -1090,13 +1113,17 @@ def test_blacklist_static(default_conf, update, mocker) -> None:
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._blacklist(bot=MagicMock(), update=update, args=[])
|
||||
telegram._blacklist(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`"
|
||||
in msg_mock.call_args_list[0][0][0])
|
||||
|
||||
msg_mock.reset_mock()
|
||||
telegram._blacklist(bot=MagicMock(), update=update, args=["ETH/BTC"])
|
||||
|
||||
# /blacklist ETH/BTC
|
||||
context = MagicMock()
|
||||
context.args = ["ETH/BTC"]
|
||||
telegram._blacklist(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`"
|
||||
in msg_mock.call_args_list[0][0][0])
|
||||
@@ -1115,7 +1142,7 @@ def test_edge_disabled(default_conf, update, mocker) -> None:
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._edge(bot=MagicMock(), update=update)
|
||||
telegram._edge(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
@@ -1137,7 +1164,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._edge(bot=MagicMock(), update=update)
|
||||
telegram._edge(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert '<b>Edge only validated following pairs:</b>\n<pre>' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -1154,7 +1181,7 @@ def test_help_handle(default_conf, update, mocker) -> None:
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._help(bot=MagicMock(), update=update)
|
||||
telegram._help(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
@@ -1169,7 +1196,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._version(bot=MagicMock(), update=update)
|
||||
telegram._version(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
@@ -1395,9 +1422,11 @@ def test__send_msg(default_conf, mocker) -> None:
|
||||
bot = MagicMock()
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
telegram._updater = MagicMock()
|
||||
telegram._updater.bot = bot
|
||||
|
||||
telegram._config['telegram']['enabled'] = True
|
||||
telegram._send_msg('test', bot)
|
||||
telegram._send_msg('test')
|
||||
assert len(bot.method_calls) == 1
|
||||
|
||||
|
||||
@@ -1407,9 +1436,11 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
|
||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
telegram._updater = MagicMock()
|
||||
telegram._updater.bot = bot
|
||||
|
||||
telegram._config['telegram']['enabled'] = True
|
||||
telegram._send_msg('test', bot)
|
||||
telegram._send_msg('test')
|
||||
|
||||
# Bot should've tried to send it twice
|
||||
assert len(bot.method_calls) == 2
|
||||
|
@@ -9,13 +9,15 @@ from freqtrade.configuration.cli_options import check_int_positive
|
||||
|
||||
# Parse common command-line-arguments. Used for all tools
|
||||
def test_parse_args_none() -> None:
|
||||
arguments = Arguments([], '')
|
||||
arguments = Arguments([])
|
||||
assert isinstance(arguments, Arguments)
|
||||
x = arguments.get_parsed_arg()
|
||||
assert isinstance(x, argparse.Namespace)
|
||||
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||
|
||||
|
||||
def test_parse_args_defaults() -> None:
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
assert args.config == ['config.json']
|
||||
assert args.strategy_path is None
|
||||
assert args.datadir is None
|
||||
@@ -23,33 +25,32 @@ def test_parse_args_defaults() -> None:
|
||||
|
||||
|
||||
def test_parse_args_config() -> None:
|
||||
args = Arguments(['-c', '/dev/null'], '').get_parsed_arg()
|
||||
args = Arguments(['-c', '/dev/null']).get_parsed_arg()
|
||||
assert args.config == ['/dev/null']
|
||||
|
||||
args = Arguments(['--config', '/dev/null'], '').get_parsed_arg()
|
||||
args = Arguments(['--config', '/dev/null']).get_parsed_arg()
|
||||
assert args.config == ['/dev/null']
|
||||
|
||||
args = Arguments(['--config', '/dev/null',
|
||||
'--config', '/dev/zero'],
|
||||
'').get_parsed_arg()
|
||||
'--config', '/dev/zero'],).get_parsed_arg()
|
||||
assert args.config == ['/dev/null', '/dev/zero']
|
||||
|
||||
|
||||
def test_parse_args_db_url() -> None:
|
||||
args = Arguments(['--db-url', 'sqlite:///test.sqlite'], '').get_parsed_arg()
|
||||
args = Arguments(['--db-url', 'sqlite:///test.sqlite']).get_parsed_arg()
|
||||
assert args.db_url == 'sqlite:///test.sqlite'
|
||||
|
||||
|
||||
def test_parse_args_verbose() -> None:
|
||||
args = Arguments(['-v'], '').get_parsed_arg()
|
||||
args = Arguments(['-v']).get_parsed_arg()
|
||||
assert args.verbosity == 1
|
||||
|
||||
args = Arguments(['--verbose'], '').get_parsed_arg()
|
||||
args = Arguments(['--verbose']).get_parsed_arg()
|
||||
assert args.verbosity == 1
|
||||
|
||||
|
||||
def test_common_scripts_options() -> None:
|
||||
args = Arguments(['download-data', '-p', 'ETH/BTC', 'XRP/BTC'], '').get_parsed_arg()
|
||||
args = Arguments(['download-data', '-p', 'ETH/BTC', 'XRP/BTC']).get_parsed_arg()
|
||||
|
||||
assert args.pairs == ['ETH/BTC', 'XRP/BTC']
|
||||
assert hasattr(args, "func")
|
||||
@@ -57,40 +58,40 @@ def test_common_scripts_options() -> None:
|
||||
|
||||
def test_parse_args_version() -> None:
|
||||
with pytest.raises(SystemExit, match=r'0'):
|
||||
Arguments(['--version'], '').get_parsed_arg()
|
||||
Arguments(['--version']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['-c'], '').get_parsed_arg()
|
||||
Arguments(['-c']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_strategy() -> None:
|
||||
args = Arguments(['--strategy', 'SomeStrategy'], '').get_parsed_arg()
|
||||
args = Arguments(['--strategy', 'SomeStrategy']).get_parsed_arg()
|
||||
assert args.strategy == 'SomeStrategy'
|
||||
|
||||
|
||||
def test_parse_args_strategy_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['--strategy'], '').get_parsed_arg()
|
||||
Arguments(['--strategy']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_strategy_path() -> None:
|
||||
args = Arguments(['--strategy-path', '/some/path'], '').get_parsed_arg()
|
||||
args = Arguments(['--strategy-path', '/some/path']).get_parsed_arg()
|
||||
assert args.strategy_path == '/some/path'
|
||||
|
||||
|
||||
def test_parse_args_strategy_path_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['--strategy-path'], '').get_parsed_arg()
|
||||
Arguments(['--strategy-path']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_backtesting_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
|
||||
Arguments(['backtesting --ticker-interval']).get_parsed_arg()
|
||||
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['backtesting --ticker-interval', 'abc'], '').get_parsed_arg()
|
||||
Arguments(['backtesting --ticker-interval', 'abc']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_backtesting_custom() -> None:
|
||||
@@ -103,7 +104,7 @@ def test_parse_args_backtesting_custom() -> None:
|
||||
'DefaultStrategy',
|
||||
'SampleStrategy'
|
||||
]
|
||||
call_args = Arguments(args, '').get_parsed_arg()
|
||||
call_args = Arguments(args).get_parsed_arg()
|
||||
assert call_args.config == ['test_conf.json']
|
||||
assert call_args.verbosity == 0
|
||||
assert call_args.subparser == 'backtesting'
|
||||
@@ -121,7 +122,7 @@ def test_parse_args_hyperopt_custom() -> None:
|
||||
'--epochs', '20',
|
||||
'--spaces', 'buy'
|
||||
]
|
||||
call_args = Arguments(args, '').get_parsed_arg()
|
||||
call_args = Arguments(args).get_parsed_arg()
|
||||
assert call_args.config == ['test_conf.json']
|
||||
assert call_args.epochs == 20
|
||||
assert call_args.verbosity == 0
|
||||
@@ -138,7 +139,7 @@ def test_download_data_options() -> None:
|
||||
'--days', '30',
|
||||
'--exchange', 'binance'
|
||||
]
|
||||
args = Arguments(args, '').get_parsed_arg()
|
||||
args = Arguments(args).get_parsed_arg()
|
||||
|
||||
assert args.pairs_file == 'file_with_pairs'
|
||||
assert args.datadir == 'datadir/directory'
|
||||
@@ -155,7 +156,7 @@ def test_plot_dataframe_options() -> None:
|
||||
'--plot-limit', '30',
|
||||
'-p', 'UNITTEST/BTC',
|
||||
]
|
||||
pargs = Arguments(args, '').get_parsed_arg()
|
||||
pargs = Arguments(args).get_parsed_arg()
|
||||
|
||||
assert pargs.indicators1 == ["sma10", "sma100"]
|
||||
assert pargs.indicators2 == ["macd", "fastd", "fastk"]
|
||||
@@ -170,7 +171,7 @@ def test_plot_profit_options() -> None:
|
||||
'--trade-source', 'DB',
|
||||
"--db-url", "sqlite:///whatever.sqlite",
|
||||
]
|
||||
pargs = Arguments(args, '').get_parsed_arg()
|
||||
pargs = Arguments(args).get_parsed_arg()
|
||||
|
||||
assert pargs.trade_source == "DB"
|
||||
assert pargs.pairs == ["UNITTEST/BTC"]
|
||||
|
@@ -66,7 +66,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
def test__args_to_config(caplog):
|
||||
|
||||
arg_list = ['--strategy-path', 'TestTest']
|
||||
args = Arguments(arg_list, '').get_parsed_arg()
|
||||
args = Arguments(arg_list).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
config = {}
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
@@ -93,7 +93,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||
default_conf['max_open_trades'] = 0
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@@ -119,7 +119,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
|
||||
)
|
||||
|
||||
arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ]
|
||||
args = Arguments(arg_list, '').get_parsed_arg()
|
||||
args = Arguments(arg_list).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@@ -167,7 +167,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
|
||||
default_conf['max_open_trades'] = -1
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@@ -191,7 +191,7 @@ def test_load_config_file_exception(mocker) -> None:
|
||||
def test_load_config(default_conf, mocker) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@@ -208,7 +208,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
'--strategy-path', '/some/path',
|
||||
'--db-url', 'sqlite:///someurl',
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@@ -226,7 +226,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
@@ -242,7 +242,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
@@ -258,7 +258,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
@@ -276,7 +276,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
@@ -290,7 +290,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||
})
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@@ -305,7 +305,7 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
||||
'--strategy', 'TestStrategy',
|
||||
'--db-url', 'sqlite:///tmp/testdb',
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
configuration.get_config()
|
||||
@@ -323,7 +323,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||
'backtesting'
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
@@ -373,7 +373,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
'--export', '/bar/foo'
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
@@ -423,7 +423,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
|
||||
'TestStrategy'
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args, RunMode.BACKTEST)
|
||||
config = configuration.get_config()
|
||||
@@ -460,7 +460,7 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
'--epochs', '10',
|
||||
'--spaces', 'all',
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args, RunMode.HYPEROPT)
|
||||
config = configuration.get_config()
|
||||
@@ -536,7 +536,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
||||
# Prevent setting loggers
|
||||
mocker.patch('freqtrade.loggers._set_loggers', MagicMock)
|
||||
arglist = ['-vvv']
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
@@ -589,7 +589,7 @@ def test_set_logfile(default_conf, mocker):
|
||||
arglist = [
|
||||
'--logfile', 'test_file.log',
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@@ -603,7 +603,7 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
|
||||
default_conf['forcebuy_enable'] = True
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@@ -778,7 +778,7 @@ def test_pairlist_resolving():
|
||||
'--exchange', 'binance'
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
@@ -794,7 +794,7 @@ def test_pairlist_resolving_with_config(mocker, default_conf):
|
||||
'download-data',
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
@@ -809,7 +809,7 @@ def test_pairlist_resolving_with_config(mocker, default_conf):
|
||||
'--pairs', 'ETH/BTC', 'XRP/BTC',
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
@@ -831,7 +831,7 @@ def test_pairlist_resolving_with_config_pl(mocker, default_conf):
|
||||
'--pairs-file', 'pairs.json',
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
@@ -853,7 +853,7 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf):
|
||||
'--pairs-file', 'pairs.json',
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
with pytest.raises(OperationalException, match=r"No pairs file found with path.*"):
|
||||
configuration = Configuration(args)
|
||||
@@ -870,7 +870,9 @@ def test_pairlist_resolving_fallback(mocker):
|
||||
'--exchange', 'binance'
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
# Fix flaky tests if config.json exists
|
||||
args.config = None
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
|
@@ -1152,7 +1152,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
side_effect=DependencyException()
|
||||
)
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert log_has('Unable to place a stoploss order on exchange: ', caplog)
|
||||
assert log_has('Unable to place a stoploss order on exchange.', caplog)
|
||||
assert trade.stoploss_order_id is None
|
||||
|
||||
# Fifth case: get_order returns InvalidOrder
|
||||
@@ -1200,6 +1200,50 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
||||
assert trade.is_open is True
|
||||
|
||||
|
||||
def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
||||
markets, limit_buy_order, limit_sell_order):
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
sell_mock = MagicMock(return_value={'id': limit_sell_order['id']})
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.00001172,
|
||||
'ask': 0.00001173,
|
||||
'last': 0.00001172
|
||||
}),
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=sell_mock,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||
stoploss_limit=MagicMock(side_effect=InvalidOrderException()),
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||
|
||||
freqtrade.create_trades()
|
||||
trade = Trade.query.first()
|
||||
caplog.clear()
|
||||
freqtrade.create_stoploss_order(trade, 200, 199)
|
||||
assert trade.stoploss_order_id is None
|
||||
assert trade.sell_reason == SellType.EMERGENCY_SELL.value
|
||||
assert log_has("Unable to place a stoploss order on exchange. ", caplog)
|
||||
assert log_has("Selling the trade forcefully", caplog)
|
||||
|
||||
# Should call a market sell
|
||||
assert sell_mock.call_count == 1
|
||||
assert sell_mock.call_args[1]['ordertype'] == 'market'
|
||||
assert sell_mock.call_args[1]['pair'] == trade.pair
|
||||
assert sell_mock.call_args[1]['amount'] == trade.amount
|
||||
|
||||
# Rpc is sending first buy, then sell
|
||||
assert rpc_mock.call_count == 2
|
||||
assert rpc_mock.call_args_list[1][0][0]['sell_reason'] == SellType.EMERGENCY_SELL.value
|
||||
assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market'
|
||||
|
||||
|
||||
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
markets, limit_buy_order, limit_sell_order) -> None:
|
||||
# When trailing stoploss is set
|
||||
|
@@ -117,7 +117,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||
|
||||
args = Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
|
||||
args = Arguments(['-c', 'config.json.example']).get_parsed_arg()
|
||||
worker = Worker(args=args, config=default_conf)
|
||||
with pytest.raises(SystemExit):
|
||||
main(['-c', 'config.json.example'])
|
||||
@@ -139,7 +139,7 @@ def test_reconfigure(mocker, default_conf) -> None:
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||
|
||||
args = Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
|
||||
args = Arguments(['-c', 'config.json.example']).get_parsed_arg()
|
||||
worker = Worker(args=args, config=default_conf)
|
||||
freqtrade = worker.freqtrade
|
||||
|
||||
|
@@ -4,8 +4,10 @@ from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import plotly.graph_objects as go
|
||||
import pytest
|
||||
from plotly.subplots import make_subplots
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
|
||||
@@ -335,6 +337,15 @@ def test_start_plot_profit(mocker):
|
||||
assert called_config['pairs'] == ["ETH/BTC"]
|
||||
|
||||
|
||||
def test_start_plot_profit_error(mocker):
|
||||
args = [
|
||||
"plot-profit",
|
||||
"--pairs", "ETH/BTC"
|
||||
]
|
||||
with pytest.raises(OperationalException):
|
||||
start_plot_profit(get_args(args))
|
||||
|
||||
|
||||
def test_plot_profit(default_conf, mocker, caplog):
|
||||
default_conf['trade_source'] = 'file'
|
||||
default_conf["datadir"] = history.make_testdata_path(None)
|
||||
|
Reference in New Issue
Block a user