Merge branch 'develop' into hyperopt-simplified-interface

This commit is contained in:
hroff-1902 2019-09-06 15:11:06 +03:00 committed by GitHub
commit 2e49125e87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 502 additions and 300 deletions

View File

@ -38,6 +38,7 @@
"order_types": { "order_types": {
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"emergencysell": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60

View File

@ -192,19 +192,20 @@ end up paying more then would probably have been necessary.
### Understand order_types ### Understand order_types
The `order_types` configuration parameter contains a dict mapping order-types to The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
market-types as well as stoploss on or off exchange type and stoploss on exchange
update interval in seconds. This allows to buy using limit orders, sell using
limit-orders, and create stoploss orders using market. It also allows to set the
stoploss "on exchange" which means stoploss order would be placed immediately once
the buy order is fulfilled. In case stoploss on exchange and `trailing_stop` are
both set, then the bot will use `stoploss_on_exchange_interval` to check it periodically
and update it if necessary (e.x. in case of trailing stoploss).
This can be set in the configuration file or in the strategy.
Values set in the configuration file overwrites values set in the strategy.
If this is configured, all 4 values (`buy`, `sell`, `stoploss` and This allows to buy using limit orders, sell using
`stoploss_on_exchange`) need to be present, otherwise the bot will warn about it and fail to start. limit-orders, and create stoplosses using using market orders. It also allows to set the
stoploss "on exchange" which means stoploss order would be placed immediately once
the buy order is fulfilled.
If `stoploss_on_exchange` and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check and update the stoploss on exchange periodically.
`order_types` can be set in the configuration file or in the strategy.
`order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place.
If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
`stoploss_on_exchange`) need to be present, otherwise the bot will fail to start.
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
The below is the default which is used if this is not configured in either strategy or configuration file. The below is the default which is used if this is not configured in either strategy or configuration file.
Syntax for Strategy: Syntax for Strategy:
@ -213,6 +214,7 @@ Syntax for Strategy:
order_types = { order_types = {
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"emergencysell": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": False, "stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
@ -225,6 +227,7 @@ Configuration:
"order_types": { "order_types": {
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"emergencysell": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
@ -239,11 +242,13 @@ Configuration:
!!! Note !!! Note
Stoploss on exchange interval is not mandatory. Do not change its value if you are Stoploss on exchange interval is not mandatory. Do not change its value if you are
unsure of what you are doing. For more information about how stoploss works please unsure of what you are doing. For more information about how stoploss works please
read [the stoploss documentation](stoploss.md). refer to [the stoploss documentation](stoploss.md).
!!! Note !!! Note
In case of stoploss on exchange if the stoploss is cancelled manually then If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
the bot would recreate one.
!!! Warning stoploss_on_exchange failures
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
### Understand order_time_in_force ### Understand order_time_in_force

View File

@ -389,18 +389,20 @@ minimal_roi = {
} }
``` ```
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps) with the values that can vary in the following ranges: If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values can vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point):
| # | minutes | ROI percentage | | # step | 1m | | 5m | | 1h | | 1d | |
|---|---|---| |---|---|---|---|---|---|---|---|---|
| 1 | always 0 | 0.03...0.31 | | 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
| 2 | 10...40 | 0.02...0.11 | | 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 |
| 3 | 20...100 | 0.01...0.04 | | 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
| 4 | 30...220 | always 0 | | 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
This structure of the ROI table is sufficient in most cases. Override the `roi_space()` method defining the ranges desired if you need components of the ROI tables to vary in other ranges. These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the ticker interval used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the ticker interval used.
Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization in these methods if you need a different structure of the ROI table or other amount of rows (steps) in the ROI tables. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py).
### Understand Hyperopt Stoploss results ### Understand Hyperopt Stoploss results
@ -422,7 +424,9 @@ Stoploss: -0.37996664668703606
If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.5...-0.02, which is sufficient in most cases. If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.5...-0.02, which is sufficient in most cases.
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default.
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py).
### Validate backtesting results ### Validate backtesting results

View File

@ -224,7 +224,7 @@ This would signify a stoploss of -10%.
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md). For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems). If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order_types dictionary, so your stoploss is on the exchange and cannot be missed due to network problems, high load or other reasons.
For more information on order_types please look [here](configuration.md#understand-order_types). For more information on order_types please look [here](configuration.md#understand-order_types).

View File

@ -3,6 +3,7 @@ This module contains the argument manager class
""" """
import argparse import argparse
from typing import List, Optional from typing import List, Optional
from pathlib import Path
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
from freqtrade import constants from freqtrade import constants
@ -47,12 +48,10 @@ class Arguments(object):
""" """
Arguments Class. Manage the arguments received by the cli Arguments Class. Manage the arguments received by the cli
""" """
def __init__(self, args: Optional[List[str]], description: str, def __init__(self, args: Optional[List[str]]) -> None:
no_default_config: bool = False) -> None:
self.args = args self.args = args
self._parsed_arg: Optional[argparse.Namespace] = None self._parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description=description) self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
self._no_default_config = no_default_config
def _load_args(self) -> None: def _load_args(self) -> None:
self._build_args(optionlist=ARGS_MAIN) self._build_args(optionlist=ARGS_MAIN)
@ -75,11 +74,13 @@ class Arguments(object):
""" """
parsed_arg = self.parser.parse_args(self.args) 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 # Workaround issue in argparse with action='append' and default value
# (see https://bugs.python.org/issue16399) # (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting) # Allow no-config for certain commands (like downloading / plotting)
if (not self._no_default_config and parsed_arg.config is None if (parsed_arg.config is None and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or
and not ('subparser' in parsed_arg and parsed_arg.subparser in NO_CONF_REQURIED)): not ('subparser' in parsed_arg and parsed_arg.subparser in NO_CONF_REQURIED))):
parsed_arg.config = [constants.DEFAULT_CONFIG] parsed_arg.config = [constants.DEFAULT_CONFIG]
return parsed_arg return parsed_arg

View File

@ -121,6 +121,7 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'}, 'stoploss_on_exchange': {'type': 'boolean'},
'stoploss_on_exchange_interval': {'type': 'number'} 'stoploss_on_exchange_interval': {'type': 'number'}

View File

@ -4,7 +4,8 @@ from typing import Dict
import ccxt import ccxt
from freqtrade import DependencyException, OperationalException, TemporaryError from freqtrade import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -66,12 +67,14 @@ class Binance(Exchange):
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(
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'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.InvalidOrder as 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'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 f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(

View File

@ -611,6 +611,33 @@ class FreqtradeBot(object):
logger.debug('Found no sell signal for %s.', trade) logger.debug('Found no sell signal for %s.', trade)
return False 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: def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
""" """
Check if trade is fulfilled in which case the stoploss Check if trade is fulfilled in which case the stoploss
@ -629,49 +656,25 @@ class FreqtradeBot(object):
except InvalidOrderException as exception: except InvalidOrderException as exception:
logger.warning('Unable to fetch stoploss order: %s', 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 is fulfilled but there is no stoploss, we add a stoploss on exchange
if (buy_order_fulfilled and not stoploss_order): if (not trade.open_order_id and not stoploss_order):
if self.edge:
stoploss = self.edge.stoploss(pair=trade.pair) stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
else:
stoploss = self.strategy.stoploss
stop_price = trade.open_rate * (1 + stoploss) stop_price = trade.open_rate * (1 + stoploss)
# limit price should be less than stop price. if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=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)
trade.stoploss_last_update = datetime.now() trade.stoploss_last_update = datetime.now()
return False 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 is canceled for some reason we add it
if stoploss_order and stoploss_order['status'] == 'canceled': if stoploss_order and stoploss_order['status'] == 'canceled':
try: if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
stoploss_order_id = self.exchange.stoploss_limit( rate=trade.stop_loss):
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)
return False return False
except DependencyException as exception: else:
trade.stoploss_order_id = None trade.stoploss_order_id = None
logger.warning('Stoploss order was cancelled, ' logger.warning('Stoploss order was cancelled, but unable to recreate one.')
'but unable to recreate one: %s', exception)
# We check if stoploss order is fulfilled # We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] == 'closed': if stoploss_order and stoploss_order['status'] == 'closed':
@ -680,7 +683,7 @@ class FreqtradeBot(object):
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['ticker_interval'])) timeframe_to_next_date(self.config['ticker_interval']))
self._notify_sell(trade) self._notify_sell(trade, "stoploss")
return True return True
# Finally we check if stoploss on exchange should be moved up because of trailing. # Finally we check if stoploss on exchange should be moved up because of trailing.
@ -714,16 +717,12 @@ class FreqtradeBot(object):
logger.exception(f"Could not cancel stoploss order {order['id']} " logger.exception(f"Could not cancel stoploss order {order['id']} "
f"for pair {trade.pair}") f"for pair {trade.pair}")
try: # Create new stoploss order
# creating the new one if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
stoploss_order_id = self.exchange.stoploss_limit( rate=trade.stop_loss):
pair=trade.pair, amount=trade.amount, return False
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99 else:
)['id'] logger.warning(f"Could not create trailing stoploss order "
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}.") f"for pair {trade.pair}.")
def _check_and_execute_sell(self, trade: Trade, sell_rate: float, def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
@ -877,9 +876,14 @@ class FreqtradeBot(object):
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") 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 # Execute sell and update trade record
order = self.exchange.sell(pair=str(trade.pair), order = self.exchange.sell(pair=str(trade.pair),
ordertype=self.strategy.order_types[sell_type], ordertype=ordertype,
amount=trade.amount, rate=limit, amount=trade.amount, rate=limit,
time_in_force=self.strategy.order_time_in_force['sell'] 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 # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval'])) 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. Sends rpc notification when a sell occured.
""" """
@ -914,7 +918,7 @@ class FreqtradeBot(object):
'pair': trade.pair, 'pair': trade.pair,
'gain': gain, 'gain': gain,
'limit': trade.close_rate_requested, 'limit': trade.close_rate_requested,
'order_type': self.strategy.order_types['sell'], 'order_type': order_type,
'amount': trade.amount, 'amount': trade.amount,
'open_rate': trade.open_rate, 'open_rate': trade.open_rate,
'current_rate': current_rate, 'current_rate': current_rate,

View File

@ -31,10 +31,7 @@ def main(sysargv: List[str] = None) -> None:
return_code: Any = 1 return_code: Any = 1
worker = None worker = None
try: try:
arguments = Arguments( arguments = Arguments(sysargv)
sysargv,
'Free, open source crypto trading bot'
)
args: Namespace = arguments.get_parsed_arg() args: Namespace = arguments.get_parsed_arg()
# A subcommand has been issued. # A subcommand has been issued.

View File

@ -114,3 +114,10 @@ def deep_merge_dicts(source, destination):
destination[key] = value destination[key] = value
return destination 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()}

View File

@ -24,8 +24,10 @@ from skopt.space import Dimension
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_data, get_timeframe from freqtrade.data.history import load_data, get_timeframe
from freqtrade.misc import round_dict
from freqtrade.optimize.backtesting import Backtesting 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.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
@ -178,9 +180,11 @@ class Hyperopt:
indent=4) indent=4)
if self.has_space('roi'): if self.has_space('roi'):
print("ROI table:") 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'): 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: def log_results(self, results) -> None:
""" """

View File

@ -2,6 +2,8 @@
IHyperOpt interface IHyperOpt interface
This module defines the interface to apply for hyperopts This module defines the interface to apply for hyperopts
""" """
import logging
import math
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Any, Callable, List from typing import Dict, Any, Callable, List
@ -10,6 +12,11 @@ from pandas import DataFrame
from skopt.space import Dimension, Integer, Real from skopt.space import Dimension, Integer, Real
from freqtrade import OperationalException 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: 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): class IHyperOpt(ABC):
""" """
Interface for freqtrade hyperopts 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: Class attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> int: value of the ticker interval to use for the strategy ticker_interval -> int: value of the ticker interval to use for the strategy
""" """
ticker_interval: str ticker_interval: str
@ -84,6 +89,83 @@ class IHyperOpt(ABC):
return roi_table 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 @staticmethod
def stoploss_space() -> List[Dimension]: def stoploss_space() -> List[Dimension]:
""" """
@ -96,19 +178,14 @@ class IHyperOpt(ABC):
Real(-0.5, -0.02, name='stoploss'), Real(-0.5, -0.02, name='stoploss'),
] ]
@staticmethod # This is needed for proper unpickling the class attribute ticker_interval
def roi_space() -> List[Dimension]: # which is set to the actual value by the resolver.
""" # Why do I still need such shamanic mantras in modern python?
Create a ROI space. def __getstate__(self):
state = self.__dict__.copy()
state['ticker_interval'] = self.ticker_interval
return state
Defines values to search for each ROI steps. def __setstate__(self, state):
You may override it in your custom Hyperopt class. self.__dict__.update(state)
""" IHyperOpt.ticker_interval = state['ticker_interval']
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'),
]

View File

@ -1,15 +1,24 @@
from argparse import Namespace from argparse import Namespace
from freqtrade import OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.utils import setup_utils_configuration 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: def start_plot_dataframe(args: Namespace) -> None:
""" """
Entrypoint for dataframe plotting Entrypoint for dataframe plotting
""" """
# Import here to avoid errors if plot-dependencies are not installed. # Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import analyse_and_plot_pairs from freqtrade.plot.plotting import analyse_and_plot_pairs
validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT) config = setup_utils_configuration(args, RunMode.PLOT)
analyse_and_plot_pairs(config) 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. # Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import plot_profit from freqtrade.plot.plotting import plot_profit
validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT) config = setup_utils_configuration(args, RunMode.PLOT)
plot_profit(config) plot_profit(config)

View File

@ -35,7 +35,7 @@ class HyperOptResolver(IResolver):
extra_dir=config.get('hyperopt_path')) extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt # 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'): if not hasattr(self.hyperopt, 'populate_buy_trend'):
logger.warning("Custom Hyperopt does not provide populate_buy_trend. " logger.warning("Custom Hyperopt does not provide populate_buy_trend. "

View File

@ -4,12 +4,12 @@
This module manage Telegram communication This module manage Telegram communication
""" """
import logging import logging
from typing import Any, Callable, Dict, List from typing import Any, Callable, Dict
from tabulate import tabulate 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.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater, CallbackContext
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.rpc import RPC, RPCException, RPCMessageType 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): def wrapper(self, *args, **kwargs):
""" Decorator logic """ """ Decorator logic """
update = kwargs.get('update') or args[1] update = kwargs.get('update') or args[0]
# Reject unauthorized messages # Reject unauthorized messages
chat_id = int(self._config['telegram']['chat_id']) chat_id = int(self._config['telegram']['chat_id'])
@ -79,7 +79,8 @@ class Telegram(RPC):
registers all known command handlers registers all known command handlers
and starts polling for message updates 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 # Register command handler and start telegram message polling
handles = [ handles = [
@ -96,7 +97,7 @@ class Telegram(RPC):
CommandHandler('reload_conf', self._reload_conf), CommandHandler('reload_conf', self._reload_conf),
CommandHandler('stopbuy', self._stopbuy), CommandHandler('stopbuy', self._stopbuy),
CommandHandler('whitelist', self._whitelist), CommandHandler('whitelist', self._whitelist),
CommandHandler('blacklist', self._blacklist, pass_args=True), CommandHandler('blacklist', self._blacklist),
CommandHandler('edge', self._edge), CommandHandler('edge', self._edge),
CommandHandler('help', self._help), CommandHandler('help', self._help),
CommandHandler('version', self._version), CommandHandler('version', self._version),
@ -175,7 +176,7 @@ class Telegram(RPC):
self._send_msg(message) self._send_msg(message)
@authorized_only @authorized_only
def _status(self, bot: Bot, update: Update) -> None: def _status(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /status. Handler for /status.
Returns the current TradeThread status Returns the current TradeThread status
@ -184,11 +185,8 @@ class Telegram(RPC):
:return: None :return: None
""" """
# Check if additional parameters are passed if 'table' in context.args:
params = update.message.text.replace('/status', '').split(' ') \ self._status_table(update, context)
if update.message.text else []
if 'table' in params:
self._status_table(bot, update)
return return
try: try:
@ -221,13 +219,13 @@ class Telegram(RPC):
messages.append("\n".join([l for l in lines if l]).format(**r)) messages.append("\n".join([l for l in lines if l]).format(**r))
for msg in messages: for msg in messages:
self._send_msg(msg, bot=bot) self._send_msg(msg)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _status_table(self, bot: Bot, update: Update) -> None: def _status_table(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /status table. Handler for /status table.
Returns the current TradeThread status in table format Returns the current TradeThread status in table format
@ -240,10 +238,10 @@ class Telegram(RPC):
message = tabulate(df_statuses, headers='keys', tablefmt='simple') message = tabulate(df_statuses, headers='keys', tablefmt='simple')
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML) self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _daily(self, bot: Bot, update: Update) -> None: def _daily(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /daily <n> Handler for /daily <n>
Returns a daily profit (in BTC) over the last n days. Returns a daily profit (in BTC) over the last n days.
@ -254,8 +252,8 @@ class Telegram(RPC):
stake_cur = self._config['stake_currency'] stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '') fiat_disp_cur = self._config.get('fiat_display_currency', '')
try: try:
timescale = int(update.message.text.replace('/daily', '').strip()) timescale = int(context.args[0])
except (TypeError, ValueError): except (TypeError, ValueError, IndexError):
timescale = 7 timescale = 7
try: try:
stats = self._rpc_daily_profit( stats = self._rpc_daily_profit(
@ -272,12 +270,12 @@ class Telegram(RPC):
], ],
tablefmt='simple') tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>' message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _profit(self, bot: Bot, update: Update) -> None: def _profit(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /profit. Handler for /profit.
Returns a cumulative profit statistics. Returns a cumulative profit statistics.
@ -317,12 +315,12 @@ class Telegram(RPC):
f"*Latest Trade opened:* `{latest_trade_date}`\n" \ f"*Latest Trade opened:* `{latest_trade_date}`\n" \
f"*Avg. Duration:* `{avg_duration}`\n" \ f"*Avg. Duration:* `{avg_duration}`\n" \
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
self._send_msg(markdown_msg, bot=bot) self._send_msg(markdown_msg)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _balance(self, bot: Bot, update: Update) -> None: def _balance(self, update: Update, context: CallbackContext) -> None:
""" Handler for /balance """ """ Handler for /balance """
try: try:
result = self._rpc_balance(self._config.get('fiat_display_currency', '')) result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
@ -339,7 +337,7 @@ class Telegram(RPC):
# Handle overflowing messsage length # Handle overflowing messsage length
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, bot=bot) self._send_msg(output)
output = curr_output output = curr_output
else: else:
output += curr_output output += curr_output
@ -347,12 +345,12 @@ class Telegram(RPC):
output += "\n*Estimated Value*:\n" \ output += "\n*Estimated Value*:\n" \
"\t`BTC: {total: .8f}`\n" \ "\t`BTC: {total: .8f}`\n" \
"\t`{symbol}: {value: .2f}`\n".format(**result) "\t`{symbol}: {value: .2f}`\n".format(**result)
self._send_msg(output, bot=bot) self._send_msg(output)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _start(self, bot: Bot, update: Update) -> None: def _start(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /start. Handler for /start.
Starts TradeThread Starts TradeThread
@ -361,10 +359,10 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_start() msg = self._rpc_start()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot) self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only @authorized_only
def _stop(self, bot: Bot, update: Update) -> None: def _stop(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /stop. Handler for /stop.
Stops TradeThread Stops TradeThread
@ -373,10 +371,10 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_stop() msg = self._rpc_stop()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot) self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only @authorized_only
def _reload_conf(self, bot: Bot, update: Update) -> None: def _reload_conf(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /reload_conf. Handler for /reload_conf.
Triggers a config file reload Triggers a config file reload
@ -385,10 +383,10 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_reload_conf() msg = self._rpc_reload_conf()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot) self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only @authorized_only
def _stopbuy(self, bot: Bot, update: Update) -> None: def _stopbuy(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /stop_buy. Handler for /stop_buy.
Sets max_open_trades to 0 and gracefully sells all open trades Sets max_open_trades to 0 and gracefully sells all open trades
@ -397,10 +395,10 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_stopbuy() msg = self._rpc_stopbuy()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot) self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only @authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None: def _forcesell(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /forcesell <id>. Handler for /forcesell <id>.
Sells the given trade at current price Sells the given trade at current price
@ -409,16 +407,16 @@ class Telegram(RPC):
:return: None :return: None
""" """
trade_id = update.message.text.replace('/forcesell', '').strip() trade_id = context.args[0] if len(context.args) > 0 else None
try: try:
msg = self._rpc_forcesell(trade_id) 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: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _forcebuy(self, bot: Bot, update: Update) -> None: def _forcebuy(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /forcebuy <asset> <price>. Handler for /forcebuy <asset> <price>.
Buys a pair trade at the given or current price Buys a pair trade at the given or current price
@ -427,16 +425,15 @@ class Telegram(RPC):
:return: None :return: None
""" """
message = update.message.text.replace('/forcebuy', '').strip().split() pair = context.args[0]
pair = message[0] price = float(context.args[1]) if len(context.args) > 1 else None
price = float(message[1]) if len(message) > 1 else None
try: try:
self._rpc_forcebuy(pair, price) self._rpc_forcebuy(pair, price)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _performance(self, bot: Bot, update: Update) -> None: def _performance(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /performance. Handler for /performance.
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
@ -455,10 +452,10 @@ class Telegram(RPC):
message = '<b>Performance:</b>\n{}'.format(stats) message = '<b>Performance:</b>\n{}'.format(stats)
self._send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _count(self, bot: Bot, update: Update) -> None: def _count(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /count. Handler for /count.
Returns the number of trades running Returns the number of trades running
@ -475,10 +472,10 @@ class Telegram(RPC):
logger.debug(message) logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _whitelist(self, bot: Bot, update: Update) -> None: def _whitelist(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /whitelist Handler for /whitelist
Shows the currently active whitelist Shows the currently active whitelist
@ -492,17 +489,17 @@ class Telegram(RPC):
logger.debug(message) logger.debug(message)
self._send_msg(message) self._send_msg(message)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _blacklist(self, bot: Bot, update: Update, args: List[str]) -> None: def _blacklist(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /blacklist Handler for /blacklist
Shows the currently active blacklist Shows the currently active blacklist
""" """
try: try:
blacklist = self._rpc_blacklist(args) blacklist = self._rpc_blacklist(context.args)
message = f"Blacklist contains {blacklist['length']} pairs\n" message = f"Blacklist contains {blacklist['length']} pairs\n"
message += f"`{', '.join(blacklist['blacklist'])}`" message += f"`{', '.join(blacklist['blacklist'])}`"
@ -510,10 +507,10 @@ class Telegram(RPC):
logger.debug(message) logger.debug(message)
self._send_msg(message) self._send_msg(message)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _edge(self, bot: Bot, update: Update) -> None: def _edge(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /edge Handler for /edge
Shows information related to Edge Shows information related to Edge
@ -522,12 +519,12 @@ class Telegram(RPC):
edge_pairs = self._rpc_edge() edge_pairs = self._rpc_edge()
edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') 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>' 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: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _help(self, bot: Bot, update: Update) -> None: def _help(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /help. Handler for /help.
Show commands of the bot Show commands of the bot
@ -559,10 +556,10 @@ class Telegram(RPC):
"*/help:* `This help message`\n" \ "*/help:* `This help message`\n" \
"*/version:* `Show version`" "*/version:* `Show version`"
self._send_msg(message, bot=bot) self._send_msg(message)
@authorized_only @authorized_only
def _version(self, bot: Bot, update: Update) -> None: def _version(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /version. Handler for /version.
Show version information Show version information
@ -570,10 +567,9 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :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, def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message
:param msg: message :param msg: message
@ -581,7 +577,6 @@ class Telegram(RPC):
:param parse_mode: telegram parse mode :param parse_mode: telegram parse mode
:return: None :return: None
""" """
bot = bot or self._updater.bot
keyboard = [['/daily', '/profit', '/balance'], keyboard = [['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'], ['/status', '/status table', '/performance'],
@ -591,7 +586,7 @@ class Telegram(RPC):
try: try:
try: try:
bot.send_message( self._updater.bot.send_message(
self._config['telegram']['chat_id'], self._config['telegram']['chat_id'],
text=msg, text=msg,
parse_mode=parse_mode, parse_mode=parse_mode,
@ -604,7 +599,7 @@ class Telegram(RPC):
'Telegram NetworkError: %s! Trying one more time.', 'Telegram NetworkError: %s! Trying one more time.',
network_err.message network_err.message
) )
bot.send_message( self._updater.bot.send_message(
self._config['telegram']['chat_id'], self._config['telegram']['chat_id'],
text=msg, text=msg,
parse_mode=parse_mode, parse_mode=parse_mode,

View File

@ -39,6 +39,7 @@ class SellType(Enum):
TRAILING_STOP_LOSS = "trailing_stop_loss" TRAILING_STOP_LOSS = "trailing_stop_loss"
SELL_SIGNAL = "sell_signal" SELL_SIGNAL = "sell_signal"
FORCE_SELL = "force_sell" FORCE_SELL = "force_sell"
EMERGENCY_SELL = "emergency_sell"
NONE = "" NONE = ""

View File

@ -45,7 +45,7 @@ def log_has_re(line, logs):
def get_args(args): 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: def patched_configuration_load_config_file(mocker, config) -> None:

View File

@ -4,7 +4,8 @@ from unittest.mock import MagicMock
import ccxt import ccxt
import pytest import pytest
from freqtrade import DependencyException, OperationalException, TemporaryError from freqtrade import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.tests.conftest import get_patched_exchange 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 = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(DependencyException): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) 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 = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)

View File

@ -418,7 +418,8 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
parallel = mocker.patch( parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', '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) patch_exchange(mocker)

View File

@ -100,7 +100,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
bot = FreqtradeBot(default_conf) bot = FreqtradeBot(default_conf)
patch_get_signal(bot, (True, False)) patch_get_signal(bot, (True, False))
dummy = DummyCls(bot) dummy = DummyCls(bot)
dummy.dummy_handler(bot=MagicMock(), update=update) dummy.dummy_handler(update=update, context=MagicMock())
assert dummy.state['called'] is True assert dummy.state['called'] is True
assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog) assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
assert not log_has('Rejected unauthorized message from: 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) bot = FreqtradeBot(default_conf)
patch_get_signal(bot, (True, False)) patch_get_signal(bot, (True, False))
dummy = DummyCls(bot) dummy = DummyCls(bot)
dummy.dummy_handler(bot=MagicMock(), update=update) dummy.dummy_handler(update=update, context=MagicMock())
assert dummy.state['called'] is False assert dummy.state['called'] is False
assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog) assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog)
assert log_has('Rejected unauthorized message from: 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)) patch_get_signal(bot, (True, False))
dummy = DummyCls(bot) dummy = DummyCls(bot)
dummy.dummy_exception(bot=MagicMock(), update=update) dummy.dummy_exception(update=update, context=MagicMock())
assert dummy.state['called'] is False assert dummy.state['called'] is False
assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog) assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
assert not log_has('Rejected unauthorized message from: 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): for _ in range(3):
freqtradebot.create_trades() freqtradebot.create_trades()
telegram._status(bot=MagicMock(), update=update) telegram._status(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
update.message.text = MagicMock() context = MagicMock()
update.message.text.replace = MagicMock(return_value='table 2 3') # /status table 2 3
telegram._status(bot=MagicMock(), update=update) context.args = ["table", "2", "3"]
telegram._status(update=update, context=context)
assert status_table.call_count == 1 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 freqtradebot.state = State.STOPPED
# Status is also enabled when 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 msg_mock.call_count == 1
assert 'no active trade' in msg_mock.call_args_list[0][0][0] assert 'no active trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
telegram._status(bot=MagicMock(), update=update) telegram._status(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'no active trade' in msg_mock.call_args_list[0][0][0] assert 'no active trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
@ -242,7 +243,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
# Create some test data # Create some test data
freqtradebot.create_trades() freqtradebot.create_trades()
# Trigger status while we have a fulfilled order for the open trade # 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 # close_rate should not be included in the message as the trade is not closed
# and no line should be empty # 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 freqtradebot.state = State.STOPPED
# Status table is also enabled when 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 msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0] assert 'no active order' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.state = State.RUNNING 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 msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0] assert 'no active order' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
@ -294,7 +295,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
# Create some test data # Create some test data
freqtradebot.create_trades() 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]) text = re.sub('</?pre>', '', msg_mock.call_args_list[-1][0][0])
line = text.split("\n") line = text.split("\n")
@ -346,8 +347,10 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
trade.is_open = False trade.is_open = False
# Try valid data # Try valid data
update.message.text = '/daily 2' # /daily 2
telegram._daily(bot=MagicMock(), update=update) context = MagicMock()
context.args = ["2"]
telegram._daily(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Daily' in msg_mock.call_args_list[0][0][0] 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] 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.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
update.message.text = '/daily 1' # /daily 1
context = MagicMock()
telegram._daily(bot=MagicMock(), update=update) 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(' 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(' 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] 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 # Try invalid data
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
update.message.text = '/daily -2' # /daily -2
telegram._daily(bot=MagicMock(), update=update) context = MagicMock()
context.args = ["-2"]
telegram._daily(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0]
# Try invalid data # Try invalid data
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
update.message.text = '/daily today' # /daily today
telegram._daily(bot=MagicMock(), update=update) 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] 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)) patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._profit(bot=MagicMock(), update=update) telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[0][0][0] assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() 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 # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
telegram._profit(bot=MagicMock(), update=update) telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0] assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock() 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.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
telegram._profit(bot=MagicMock(), update=update) telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0] 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] 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 = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update) telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*BTC:*' in result assert '*BTC:*' in result
@ -536,7 +544,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.config['dry_run'] = False 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] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'All balances are zero.' in result 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 = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update) telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert "Running in Dry Run, balances are not available." in result 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 = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update) telegram._balance(update=update, context=MagicMock())
assert msg_mock.call_count > 1 assert msg_mock.call_count > 1
# Test if wrap happens around 4000 - # Test if wrap happens around 4000 -
# and each single currency-output is around 120 characters long so we need # 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 freqtradebot.state = State.STOPPED
assert 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 freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 1 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 freqtradebot.state = State.RUNNING
assert 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 freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'already running' in msg_mock.call_args_list[0][0][0] 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 freqtradebot.state = State.RUNNING
assert 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 freqtradebot.state == State.STOPPED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'stopping trader' in msg_mock.call_args_list[0][0][0] 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 freqtradebot.state = State.STOPPED
assert 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 freqtradebot.state == State.STOPPED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'already stopped' in msg_mock.call_args_list[0][0][0] 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) telegram = Telegram(freqtradebot)
assert freqtradebot.config['max_open_trades'] != 0 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 freqtradebot.config['max_open_trades'] == 0
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'No more buy will occur from now. Run /reload_conf to reset.' \ 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 freqtradebot.state = State.RUNNING
assert 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 freqtradebot.state == State.RELOAD_CONF
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'reloading config' in msg_mock.call_args_list[0][0][0] 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 # Increase the price and sell it
mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up) mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up)
update.message.text = '/forcesell 1' # /forcesell 1
telegram._forcesell(bot=MagicMock(), update=update) context = MagicMock()
context.args = ["1"]
telegram._forcesell(update=update, context=context)
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0] 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() trade = Trade.query.first()
assert trade assert trade
update.message.text = '/forcesell 1' # /forcesell 1
telegram._forcesell(bot=MagicMock(), update=update) context = MagicMock()
context.args = ["1"]
telegram._forcesell(update=update, context=context)
assert rpc_mock.call_count == 2 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() freqtradebot.create_trades()
rpc_mock.reset_mock() rpc_mock.reset_mock()
update.message.text = '/forcesell all' # /forcesell all
telegram._forcesell(bot=MagicMock(), update=update) context = MagicMock()
context.args = ["all"]
telegram._forcesell(update=update, context=context)
assert rpc_mock.call_count == 4 assert rpc_mock.call_count == 4
msg = rpc_mock.call_args_list[0][0][0] 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 # Trader is not running
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
update.message.text = '/forcesell 1' # /forcesell 1
telegram._forcesell(bot=MagicMock(), update=update) context = MagicMock()
context.args = ["1"]
telegram._forcesell(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0] assert 'not running' in msg_mock.call_args_list[0][0][0]
# No argument # No argument
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
update.message.text = '/forcesell' context = MagicMock()
telegram._forcesell(bot=MagicMock(), update=update) context.args = []
telegram._forcesell(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'invalid argument' in msg_mock.call_args_list[0][0][0] assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
# Invalid argument # Invalid argument
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
update.message.text = '/forcesell 123456' # /forcesell 123456
telegram._forcesell(bot=MagicMock(), update=update) context = MagicMock()
context.args = ["123456"]
telegram._forcesell(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'invalid argument' in msg_mock.call_args_list[0][0][0] 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)) patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
update.message.text = '/forcebuy ETH/BTC' # /forcebuy ETH/BTC
telegram._forcebuy(bot=MagicMock(), update=update) context = MagicMock()
context.args = ["ETH/BTC"]
telegram._forcebuy(update=update, context=context)
assert fbuy_mock.call_count == 1 assert fbuy_mock.call_count == 1
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' 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 # Reset and retry with specified price
fbuy_mock = MagicMock(return_value=None) fbuy_mock = MagicMock(return_value=None)
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
update.message.text = '/forcebuy ETH/BTC 0.055' # /forcebuy ETH/BTC 0.055
telegram._forcebuy(bot=MagicMock(), update=update) context = MagicMock()
context.args = ["ETH/BTC", "0.055"]
telegram._forcebuy(update=update, context=context)
assert fbuy_mock.call_count == 1 assert fbuy_mock.call_count == 1
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' 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) telegram = Telegram(freqtradebot)
update.message.text = '/forcebuy ETH/Nonepair' 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_count == 1
assert rpc_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' 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.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
telegram._performance(bot=MagicMock(), update=update) telegram._performance(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][0][0] 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] 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) telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
telegram._count(bot=MagicMock(), update=update) telegram._count(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0] assert 'not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
@ -1030,7 +1053,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
# Create some test data # Create some test data
freqtradebot.create_trades() freqtradebot.create_trades()
msg_mock.reset_mock() msg_mock.reset_mock()
telegram._count(bot=MagicMock(), update=update) telegram._count(update=update, context=MagicMock())
msg = '<pre> current max total stake\n--------- ----- -------------\n' \ msg = '<pre> current max total stake\n--------- ----- -------------\n' \
' 1 {} {}</pre>'\ ' 1 {} {}</pre>'\
@ -1052,7 +1075,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._whitelist(bot=MagicMock(), update=update) telegram._whitelist(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert ('Using whitelist `StaticPairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' 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]) 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 = Telegram(freqtradebot)
telegram._whitelist(bot=MagicMock(), update=update) telegram._whitelist(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert ('Using whitelist `VolumePairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' 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]) 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 = Telegram(freqtradebot)
telegram._blacklist(bot=MagicMock(), update=update, args=[]) telegram._blacklist(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`" assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`"
in msg_mock.call_args_list[0][0][0]) in msg_mock.call_args_list[0][0][0])
msg_mock.reset_mock() 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 msg_mock.call_count == 1
assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`" assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`"
in msg_mock.call_args_list[0][0][0]) 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 = Telegram(freqtradebot)
telegram._edge(bot=MagicMock(), update=update) telegram._edge(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0] 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 = Telegram(freqtradebot)
telegram._edge(bot=MagicMock(), update=update) telegram._edge(update=update, context=MagicMock())
assert msg_mock.call_count == 1 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 '<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] 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 = Telegram(freqtradebot)
telegram._help(bot=MagicMock(), update=update) telegram._help(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0] 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) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._version(bot=MagicMock(), update=update) telegram._version(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] 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() bot = MagicMock()
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._updater = MagicMock()
telegram._updater.bot = bot
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
telegram._send_msg('test', bot) telegram._send_msg('test')
assert len(bot.method_calls) == 1 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')) bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._updater = MagicMock()
telegram._updater.bot = bot
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
telegram._send_msg('test', bot) telegram._send_msg('test')
# Bot should've tried to send it twice # Bot should've tried to send it twice
assert len(bot.method_calls) == 2 assert len(bot.method_calls) == 2

View File

@ -9,13 +9,15 @@ from freqtrade.configuration.cli_options import check_int_positive
# Parse common command-line-arguments. Used for all tools # Parse common command-line-arguments. Used for all tools
def test_parse_args_none() -> None: def test_parse_args_none() -> None:
arguments = Arguments([], '') arguments = Arguments([])
assert isinstance(arguments, Arguments) assert isinstance(arguments, Arguments)
x = arguments.get_parsed_arg()
assert isinstance(x, argparse.Namespace)
assert isinstance(arguments.parser, argparse.ArgumentParser) assert isinstance(arguments.parser, argparse.ArgumentParser)
def test_parse_args_defaults() -> None: def test_parse_args_defaults() -> None:
args = Arguments([], '').get_parsed_arg() args = Arguments([]).get_parsed_arg()
assert args.config == ['config.json'] assert args.config == ['config.json']
assert args.strategy_path is None assert args.strategy_path is None
assert args.datadir is None assert args.datadir is None
@ -23,33 +25,32 @@ def test_parse_args_defaults() -> None:
def test_parse_args_config() -> 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'] 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'] assert args.config == ['/dev/null']
args = Arguments(['--config', '/dev/null', args = Arguments(['--config', '/dev/null',
'--config', '/dev/zero'], '--config', '/dev/zero'],).get_parsed_arg()
'').get_parsed_arg()
assert args.config == ['/dev/null', '/dev/zero'] assert args.config == ['/dev/null', '/dev/zero']
def test_parse_args_db_url() -> None: 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' assert args.db_url == 'sqlite:///test.sqlite'
def test_parse_args_verbose() -> None: def test_parse_args_verbose() -> None:
args = Arguments(['-v'], '').get_parsed_arg() args = Arguments(['-v']).get_parsed_arg()
assert args.verbosity == 1 assert args.verbosity == 1
args = Arguments(['--verbose'], '').get_parsed_arg() args = Arguments(['--verbose']).get_parsed_arg()
assert args.verbosity == 1 assert args.verbosity == 1
def test_common_scripts_options() -> None: 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 args.pairs == ['ETH/BTC', 'XRP/BTC']
assert hasattr(args, "func") assert hasattr(args, "func")
@ -57,40 +58,40 @@ def test_common_scripts_options() -> None:
def test_parse_args_version() -> None: def test_parse_args_version() -> None:
with pytest.raises(SystemExit, match=r'0'): with pytest.raises(SystemExit, match=r'0'):
Arguments(['--version'], '').get_parsed_arg() Arguments(['--version']).get_parsed_arg()
def test_parse_args_invalid() -> None: def test_parse_args_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'): with pytest.raises(SystemExit, match=r'2'):
Arguments(['-c'], '').get_parsed_arg() Arguments(['-c']).get_parsed_arg()
def test_parse_args_strategy() -> None: def test_parse_args_strategy() -> None:
args = Arguments(['--strategy', 'SomeStrategy'], '').get_parsed_arg() args = Arguments(['--strategy', 'SomeStrategy']).get_parsed_arg()
assert args.strategy == 'SomeStrategy' assert args.strategy == 'SomeStrategy'
def test_parse_args_strategy_invalid() -> None: def test_parse_args_strategy_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'): with pytest.raises(SystemExit, match=r'2'):
Arguments(['--strategy'], '').get_parsed_arg() Arguments(['--strategy']).get_parsed_arg()
def test_parse_args_strategy_path() -> None: 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' assert args.strategy_path == '/some/path'
def test_parse_args_strategy_path_invalid() -> None: def test_parse_args_strategy_path_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'): 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: def test_parse_args_backtesting_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'): 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'): 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: def test_parse_args_backtesting_custom() -> None:
@ -103,7 +104,7 @@ def test_parse_args_backtesting_custom() -> None:
'DefaultStrategy', 'DefaultStrategy',
'SampleStrategy' '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.config == ['test_conf.json']
assert call_args.verbosity == 0 assert call_args.verbosity == 0
assert call_args.subparser == 'backtesting' assert call_args.subparser == 'backtesting'
@ -121,7 +122,7 @@ def test_parse_args_hyperopt_custom() -> None:
'--epochs', '20', '--epochs', '20',
'--spaces', 'buy' '--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.config == ['test_conf.json']
assert call_args.epochs == 20 assert call_args.epochs == 20
assert call_args.verbosity == 0 assert call_args.verbosity == 0
@ -138,7 +139,7 @@ def test_download_data_options() -> None:
'--days', '30', '--days', '30',
'--exchange', 'binance' '--exchange', 'binance'
] ]
args = Arguments(args, '').get_parsed_arg() args = Arguments(args).get_parsed_arg()
assert args.pairs_file == 'file_with_pairs' assert args.pairs_file == 'file_with_pairs'
assert args.datadir == 'datadir/directory' assert args.datadir == 'datadir/directory'
@ -155,7 +156,7 @@ def test_plot_dataframe_options() -> None:
'--plot-limit', '30', '--plot-limit', '30',
'-p', 'UNITTEST/BTC', '-p', 'UNITTEST/BTC',
] ]
pargs = Arguments(args, '').get_parsed_arg() pargs = Arguments(args).get_parsed_arg()
assert pargs.indicators1 == ["sma10", "sma100"] assert pargs.indicators1 == ["sma10", "sma100"]
assert pargs.indicators2 == ["macd", "fastd", "fastk"] assert pargs.indicators2 == ["macd", "fastd", "fastk"]
@ -170,7 +171,7 @@ def test_plot_profit_options() -> None:
'--trade-source', 'DB', '--trade-source', 'DB',
"--db-url", "sqlite:///whatever.sqlite", "--db-url", "sqlite:///whatever.sqlite",
] ]
pargs = Arguments(args, '').get_parsed_arg() pargs = Arguments(args).get_parsed_arg()
assert pargs.trade_source == "DB" assert pargs.trade_source == "DB"
assert pargs.pairs == ["UNITTEST/BTC"] assert pargs.pairs == ["UNITTEST/BTC"]

View File

@ -66,7 +66,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
def test__args_to_config(caplog): def test__args_to_config(caplog):
arg_list = ['--strategy-path', 'TestTest'] arg_list = ['--strategy-path', 'TestTest']
args = Arguments(arg_list, '').get_parsed_arg() args = Arguments(arg_list).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = {} config = {}
with warnings.catch_warnings(record=True) as w: 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 default_conf['max_open_trades'] = 0
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([], '').get_parsed_arg() args = Arguments([]).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() 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', ] 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) configuration = Configuration(args)
validated_conf = configuration.load_config() 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 default_conf['max_open_trades'] = -1
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([], '').get_parsed_arg() args = Arguments([]).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() 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: def test_load_config(default_conf, mocker) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([], '').get_parsed_arg() args = Arguments([]).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -208,7 +208,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
'--strategy-path', '/some/path', '--strategy-path', '/some/path',
'--db-url', 'sqlite:///someurl', '--db-url', 'sqlite:///someurl',
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -226,7 +226,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path' '--strategy-path', '/some/path'
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -242,7 +242,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path' '--strategy-path', '/some/path'
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -258,7 +258,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path' '--strategy-path', '/some/path'
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -276,7 +276,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path' '--strategy-path', '/some/path'
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() 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) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([], '').get_parsed_arg() args = Arguments([]).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -305,7 +305,7 @@ def test_show_info(default_conf, mocker, caplog) -> None:
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--db-url', 'sqlite:///tmp/testdb', '--db-url', 'sqlite:///tmp/testdb',
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
configuration.get_config() configuration.get_config()
@ -323,7 +323,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
'backtesting' 'backtesting'
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()
@ -373,7 +373,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--export', '/bar/foo' '--export', '/bar/foo'
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()
@ -423,7 +423,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
'TestStrategy' 'TestStrategy'
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args, RunMode.BACKTEST) configuration = Configuration(args, RunMode.BACKTEST)
config = configuration.get_config() config = configuration.get_config()
@ -460,7 +460,7 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
'--epochs', '10', '--epochs', '10',
'--spaces', 'all', '--spaces', 'all',
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args, RunMode.HYPEROPT) configuration = Configuration(args, RunMode.HYPEROPT)
config = configuration.get_config() config = configuration.get_config()
@ -536,7 +536,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
# Prevent setting loggers # Prevent setting loggers
mocker.patch('freqtrade.loggers._set_loggers', MagicMock) mocker.patch('freqtrade.loggers._set_loggers', MagicMock)
arglist = ['-vvv'] arglist = ['-vvv']
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -589,7 +589,7 @@ def test_set_logfile(default_conf, mocker):
arglist = [ arglist = [
'--logfile', 'test_file.log', '--logfile', 'test_file.log',
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() 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 default_conf['forcebuy_enable'] = True
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([], '').get_parsed_arg() args = Arguments([]).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -778,7 +778,7 @@ def test_pairlist_resolving():
'--exchange', 'binance' '--exchange', 'binance'
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()
@ -794,7 +794,7 @@ def test_pairlist_resolving_with_config(mocker, default_conf):
'download-data', 'download-data',
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()
@ -809,7 +809,7 @@ def test_pairlist_resolving_with_config(mocker, default_conf):
'--pairs', 'ETH/BTC', 'XRP/BTC', '--pairs', 'ETH/BTC', 'XRP/BTC',
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()
@ -831,7 +831,7 @@ def test_pairlist_resolving_with_config_pl(mocker, default_conf):
'--pairs-file', 'pairs.json', '--pairs-file', 'pairs.json',
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()
@ -853,7 +853,7 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf):
'--pairs-file', 'pairs.json', '--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.*"): with pytest.raises(OperationalException, match=r"No pairs file found with path.*"):
configuration = Configuration(args) configuration = Configuration(args)
@ -870,7 +870,9 @@ def test_pairlist_resolving_fallback(mocker):
'--exchange', 'binance' '--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) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()

View File

@ -1152,7 +1152,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
side_effect=DependencyException() side_effect=DependencyException()
) )
freqtrade.handle_stoploss_on_exchange(trade) 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 assert trade.stoploss_order_id is None
# Fifth case: get_order returns InvalidOrder # 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 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, def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
markets, limit_buy_order, limit_sell_order) -> None: markets, limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set # When trailing stoploss is set

View File

@ -117,7 +117,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', 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) worker = Worker(args=args, config=default_conf)
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(['-c', 'config.json.example']) 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.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', 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) worker = Worker(args=args, config=default_conf)
freqtrade = worker.freqtrade freqtrade = worker.freqtrade

View File

@ -4,8 +4,10 @@ from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
import plotly.graph_objects as go import plotly.graph_objects as go
import pytest
from plotly.subplots import make_subplots from plotly.subplots import make_subplots
from freqtrade import OperationalException
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data 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"] 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): def test_plot_profit(default_conf, mocker, caplog):
default_conf['trade_source'] = 'file' default_conf['trade_source'] = 'file'
default_conf["datadir"] = history.make_testdata_path(None) default_conf["datadir"] = history.make_testdata_path(None)

View File

@ -2,7 +2,7 @@
# mainly used for Raspberry pi installs # mainly used for Raspberry pi installs
ccxt==1.18.1115 ccxt==1.18.1115
SQLAlchemy==1.3.8 SQLAlchemy==1.3.8
python-telegram-bot==11.1.0 python-telegram-bot==12.0.0
arrow==0.14.6 arrow==0.14.6
cachetools==3.1.1 cachetools==3.1.1
requests==2.22.0 requests==2.22.0