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": {
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"stoploss": "market",
"stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60

View File

@ -192,19 +192,20 @@ end up paying more then would probably have been necessary.
### Understand order_types
The `order_types` configuration parameter contains a dict mapping order-types to
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.
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.
If this is configured, all 4 values (`buy`, `sell`, `stoploss` and
`stoploss_on_exchange`) need to be present, otherwise the bot will warn about it and fail to start.
This allows to buy using limit orders, sell using
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.
Syntax for Strategy:
@ -213,6 +214,7 @@ Syntax for Strategy:
order_types = {
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"stoploss": "market",
"stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60
@ -225,6 +227,7 @@ Configuration:
"order_types": {
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"stoploss": "market",
"stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60
@ -239,11 +242,13 @@ Configuration:
!!! Note
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
read [the stoploss documentation](stoploss.md).
refer to [the stoploss documentation](stoploss.md).
!!! Note
In case of stoploss on exchange if the stoploss is cancelled manually then
the bot would recreate one.
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
!!! 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

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 |
|---|---|---|
| 1 | always 0 | 0.03...0.31 |
| 2 | 10...40 | 0.02...0.11 |
| 3 | 20...100 | 0.01...0.04 |
| 4 | 30...220 | always 0 |
| # step | 1m | | 5m | | 1h | | 1d | |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
| 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 | 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 | 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
@ -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.
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

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).
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).

View File

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

View File

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

View File

@ -4,7 +4,8 @@ from typing import Dict
import ccxt
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
@ -66,12 +67,14 @@ class Binance(Exchange):
except ccxt.InsufficientFunds as e:
raise DependencyException(
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate}.'
f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
raise DependencyException(
# Errors:
# `binance Order would trigger immediately.`
raise InvalidOrderException(
f'Could not create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}.'
f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(

View File

@ -611,6 +611,33 @@ class FreqtradeBot(object):
logger.debug('Found no sell signal for %s.', trade)
return False
def create_stoploss_order(self, trade: Trade, stop_price: float, rate: float) -> bool:
"""
Abstracts creating stoploss orders from the logic.
Handles errors and updates the trade database object.
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
:return: True if the order succeeded, and False in case of problems.
"""
# Limit price threshold: As limit price should always be below price
LIMIT_PRICE_PCT = 0.99
try:
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
stop_price=stop_price,
rate=rate * LIMIT_PRICE_PCT)
trade.stoploss_order_id = str(stoploss_order['id'])
return True
except InvalidOrderException as e:
trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Selling the trade forcefully')
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
except DependencyException:
trade.stoploss_order_id = None
logger.exception('Unable to place a stoploss order on exchange.')
return False
def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
"""
Check if trade is fulfilled in which case the stoploss
@ -629,49 +656,25 @@ class FreqtradeBot(object):
except InvalidOrderException as exception:
logger.warning('Unable to fetch stoploss order: %s', exception)
# If trade open order id does not exist: buy order is fulfilled
buy_order_fulfilled = not trade.open_order_id
# Limit price threshold: As limit price should always be below price
limit_price_pct = 0.99
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
if (buy_order_fulfilled and not stoploss_order):
if self.edge:
stoploss = self.edge.stoploss(pair=trade.pair)
else:
stoploss = self.strategy.stoploss
if (not trade.open_order_id and not stoploss_order):
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
stop_price = trade.open_rate * (1 + stoploss)
# limit price should be less than stop price.
limit_price = stop_price * limit_price_pct
try:
stoploss_order_id = self.exchange.stoploss_limit(
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
)['id']
trade.stoploss_order_id = str(stoploss_order_id)
if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
trade.stoploss_last_update = datetime.now()
return False
except DependencyException as exception:
trade.stoploss_order_id = None
logger.warning('Unable to place a stoploss order on exchange: %s', exception)
# If stoploss order is canceled for some reason we add it
if stoploss_order and stoploss_order['status'] == 'canceled':
try:
stoploss_order_id = self.exchange.stoploss_limit(
pair=trade.pair, amount=trade.amount,
stop_price=trade.stop_loss, rate=trade.stop_loss * limit_price_pct
)['id']
trade.stoploss_order_id = str(stoploss_order_id)
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
rate=trade.stop_loss):
return False
except DependencyException as exception:
else:
trade.stoploss_order_id = None
logger.warning('Stoploss order was cancelled, '
'but unable to recreate one: %s', exception)
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
# We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] == 'closed':
@ -680,7 +683,7 @@ class FreqtradeBot(object):
# Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['ticker_interval']))
self._notify_sell(trade)
self._notify_sell(trade, "stoploss")
return True
# Finally we check if stoploss on exchange should be moved up because of trailing.
@ -714,17 +717,13 @@ class FreqtradeBot(object):
logger.exception(f"Could not cancel stoploss order {order['id']} "
f"for pair {trade.pair}")
try:
# creating the new one
stoploss_order_id = self.exchange.stoploss_limit(
pair=trade.pair, amount=trade.amount,
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
)['id']
trade.stoploss_order_id = str(stoploss_order_id)
except DependencyException:
trade.stoploss_order_id = None
logger.exception(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.")
# Create new stoploss order
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
rate=trade.stop_loss):
return False
else:
logger.warning(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.")
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
buy: bool, sell: bool) -> bool:
@ -877,9 +876,14 @@ class FreqtradeBot(object):
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
ordertype = self.strategy.order_types[sell_type]
if sell_reason == SellType.EMERGENCY_SELL:
# Emergencysells (default to market!)
ordertype = self.strategy.order_types.get("emergencysell", "market")
# Execute sell and update trade record
order = self.exchange.sell(pair=str(trade.pair),
ordertype=self.strategy.order_types[sell_type],
ordertype=ordertype,
amount=trade.amount, rate=limit,
time_in_force=self.strategy.order_time_in_force['sell']
)
@ -895,9 +899,9 @@ class FreqtradeBot(object):
# Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
self._notify_sell(trade)
self._notify_sell(trade, ordertype)
def _notify_sell(self, trade: Trade):
def _notify_sell(self, trade: Trade, order_type: str):
"""
Sends rpc notification when a sell occured.
"""
@ -914,7 +918,7 @@ class FreqtradeBot(object):
'pair': trade.pair,
'gain': gain,
'limit': trade.close_rate_requested,
'order_type': self.strategy.order_types['sell'],
'order_type': order_type,
'amount': trade.amount,
'open_rate': trade.open_rate,
'current_rate': current_rate,

View File

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

View File

@ -114,3 +114,10 @@ def deep_merge_dicts(source, destination):
destination[key] = value
return destination
def round_dict(d, n):
"""
Rounds float values in the dict to n digits after the decimal point.
"""
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}

View File

@ -24,8 +24,10 @@ from skopt.space import Dimension
from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_data, get_timeframe
from freqtrade.misc import round_dict
from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOptLoss to allow users import from this file
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
@ -178,9 +180,11 @@ class Hyperopt:
indent=4)
if self.has_space('roi'):
print("ROI table:")
pprint(self.custom_hyperopt.generate_roi_table(params), indent=4)
# Round printed values to 5 digits after the decimal point
pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4)
if self.has_space('stoploss'):
print(f"Stoploss: {params.get('stoploss')}")
# Also round to 5 digits after the decimal point
print(f"Stoploss: {round(params.get('stoploss'), 5)}")
def log_results(self, results) -> None:
"""

View File

@ -2,6 +2,8 @@
IHyperOpt interface
This module defines the interface to apply for hyperopts
"""
import logging
import math
from abc import ABC, abstractmethod
from typing import Dict, Any, Callable, List
@ -10,6 +12,11 @@ from pandas import DataFrame
from skopt.space import Dimension, Integer, Real
from freqtrade import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import round_dict
logger = logging.getLogger(__name__)
def _format_exception_message(method: str, space: str) -> str:
@ -22,11 +29,9 @@ def _format_exception_message(method: str, space: str) -> str:
class IHyperOpt(ABC):
"""
Interface for freqtrade hyperopts
Defines the mandatory structure must follow any custom strategies
Defines the mandatory structure must follow any custom hyperopts
Attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy
Class attributes you can use:
ticker_interval -> int: value of the ticker interval to use for the strategy
"""
ticker_interval: str
@ -84,6 +89,83 @@ class IHyperOpt(ABC):
return roi_table
@staticmethod
def roi_space() -> List[Dimension]:
"""
Create a ROI space.
Defines values to search for each ROI steps.
This method implements adaptive roi hyperspace with varied
ranges for parameters which automatically adapts to the
ticker interval used.
It's used by Freqtrade by default, if no custom roi_space method is defined.
"""
# Default scaling coefficients for the roi hyperspace. Can be changed
# to adjust resulting ranges of the ROI tables.
# Increase if you need wider ranges in the roi hyperspace, decrease if shorter
# ranges are needed.
roi_t_alpha = 1.0
roi_p_alpha = 1.0
ticker_interval_mins = timeframe_to_minutes(IHyperOpt.ticker_interval)
# We define here limits for the ROI space parameters automagically adapted to the
# ticker_interval used by the bot:
#
# * 'roi_t' (limits for the time intervals in the ROI tables) components
# are scaled linearly.
# * 'roi_p' (limits for the ROI value steps) components are scaled logarithmically.
#
# The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space()
# method for the 5m ticker interval.
roi_t_scale = ticker_interval_mins / 5
roi_p_scale = math.log1p(ticker_interval_mins) / math.log1p(5)
roi_limits = {
'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha),
'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha),
'roi_t2_min': int(10 * roi_t_scale * roi_t_alpha),
'roi_t2_max': int(60 * roi_t_scale * roi_t_alpha),
'roi_t3_min': int(10 * roi_t_scale * roi_t_alpha),
'roi_t3_max': int(40 * roi_t_scale * roi_t_alpha),
'roi_p1_min': 0.01 * roi_p_scale * roi_p_alpha,
'roi_p1_max': 0.04 * roi_p_scale * roi_p_alpha,
'roi_p2_min': 0.01 * roi_p_scale * roi_p_alpha,
'roi_p2_max': 0.07 * roi_p_scale * roi_p_alpha,
'roi_p3_min': 0.01 * roi_p_scale * roi_p_alpha,
'roi_p3_max': 0.20 * roi_p_scale * roi_p_alpha,
}
logger.debug(f"Using roi space limits: {roi_limits}")
p = {
'roi_t1': roi_limits['roi_t1_min'],
'roi_t2': roi_limits['roi_t2_min'],
'roi_t3': roi_limits['roi_t3_min'],
'roi_p1': roi_limits['roi_p1_min'],
'roi_p2': roi_limits['roi_p2_min'],
'roi_p3': roi_limits['roi_p3_min'],
}
logger.info(f"Min roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}")
p = {
'roi_t1': roi_limits['roi_t1_max'],
'roi_t2': roi_limits['roi_t2_max'],
'roi_t3': roi_limits['roi_t3_max'],
'roi_p1': roi_limits['roi_p1_max'],
'roi_p2': roi_limits['roi_p2_max'],
'roi_p3': roi_limits['roi_p3_max'],
}
logger.info(f"Max roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}")
return [
Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'),
Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'),
Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'),
Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'),
Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'),
Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'),
]
@staticmethod
def stoploss_space() -> List[Dimension]:
"""
@ -96,19 +178,14 @@ class IHyperOpt(ABC):
Real(-0.5, -0.02, name='stoploss'),
]
@staticmethod
def roi_space() -> List[Dimension]:
"""
Create a ROI space.
# This is needed for proper unpickling the class attribute ticker_interval
# which is set to the actual value by the resolver.
# Why do I still need such shamanic mantras in modern python?
def __getstate__(self):
state = self.__dict__.copy()
state['ticker_interval'] = self.ticker_interval
return state
Defines values to search for each ROI steps.
You may override it in your custom Hyperopt class.
"""
return [
Integer(10, 120, name='roi_t1'),
Integer(10, 60, name='roi_t2'),
Integer(10, 40, name='roi_t3'),
Real(0.01, 0.04, name='roi_p1'),
Real(0.01, 0.07, name='roi_p2'),
Real(0.01, 0.20, name='roi_p3'),
]
def __setstate__(self, state):
self.__dict__.update(state)
IHyperOpt.ticker_interval = state['ticker_interval']

View File

@ -1,15 +1,24 @@
from argparse import Namespace
from freqtrade import OperationalException
from freqtrade.state import RunMode
from freqtrade.utils import setup_utils_configuration
def validate_plot_args(args: Namespace):
args_tmp = vars(args)
if not args_tmp.get('datadir') and not args_tmp.get('config'):
raise OperationalException(
"You need to specify either `--datadir` or `--config` "
"for plot-profit and plot-dataframe.")
def start_plot_dataframe(args: Namespace) -> None:
"""
Entrypoint for dataframe plotting
"""
# Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import analyse_and_plot_pairs
validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT)
analyse_and_plot_pairs(config)
@ -21,6 +30,7 @@ def start_plot_profit(args: Namespace) -> None:
"""
# Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import plot_profit
validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT)
plot_profit(config)

View File

@ -35,7 +35,7 @@ class HyperOptResolver(IResolver):
extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt
self.hyperopt.__class__.ticker_interval = str(config['ticker_interval'])
IHyperOpt.ticker_interval = str(config['ticker_interval'])
if not hasattr(self.hyperopt, 'populate_buy_trend'):
logger.warning("Custom Hyperopt does not provide populate_buy_trend. "

View File

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

View File

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

View File

@ -45,7 +45,7 @@ def log_has_re(line, logs):
def get_args(args):
return Arguments(args, '').get_parsed_arg()
return Arguments(args).get_parsed_arg()
def patched_configuration_load_config_file(mocker, config) -> None:

View File

@ -4,7 +4,8 @@ from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.tests.conftest import get_patched_exchange
@ -49,8 +50,9 @@ def test_stoploss_limit_order(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)

View File

@ -418,7 +418,8 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result',
'params': {'buy': {}, 'sell': {}, 'roi': {}, 'stoploss': 0.0}}])
)
patch_exchange(mocker)

View File

@ -100,7 +100,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
bot = FreqtradeBot(default_conf)
patch_get_signal(bot, (True, False))
dummy = DummyCls(bot)
dummy.dummy_handler(bot=MagicMock(), update=update)
dummy.dummy_handler(update=update, context=MagicMock())
assert dummy.state['called'] is True
assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
assert not log_has('Rejected unauthorized message from: 0', caplog)
@ -117,7 +117,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
bot = FreqtradeBot(default_conf)
patch_get_signal(bot, (True, False))
dummy = DummyCls(bot)
dummy.dummy_handler(bot=MagicMock(), update=update)
dummy.dummy_handler(update=update, context=MagicMock())
assert dummy.state['called'] is False
assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog)
assert log_has('Rejected unauthorized message from: 3735928559', caplog)
@ -136,7 +136,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
patch_get_signal(bot, (True, False))
dummy = DummyCls(bot)
dummy.dummy_exception(bot=MagicMock(), update=update)
dummy.dummy_exception(update=update, context=MagicMock())
assert dummy.state['called'] is False
assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
assert not log_has('Rejected unauthorized message from: 0', caplog)
@ -194,12 +194,13 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
for _ in range(3):
freqtradebot.create_trades()
telegram._status(bot=MagicMock(), update=update)
telegram._status(update=update, context=MagicMock())
assert msg_mock.call_count == 1
update.message.text = MagicMock()
update.message.text.replace = MagicMock(return_value='table 2 3')
telegram._status(bot=MagicMock(), update=update)
context = MagicMock()
# /status table 2 3
context.args = ["table", "2", "3"]
telegram._status(update=update, context=context)
assert status_table.call_count == 1
@ -228,13 +229,13 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
freqtradebot.state = State.STOPPED
# Status is also enabled when stopped
telegram._status(bot=MagicMock(), update=update)
telegram._status(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
telegram._status(bot=MagicMock(), update=update)
telegram._status(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
@ -242,7 +243,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
# Create some test data
freqtradebot.create_trades()
# Trigger status while we have a fulfilled order for the open trade
telegram._status(bot=MagicMock(), update=update)
telegram._status(update=update, context=MagicMock())
# close_rate should not be included in the message as the trade is not closed
# and no line should be empty
@ -280,13 +281,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
freqtradebot.state = State.STOPPED
# Status table is also enabled when stopped
telegram._status_table(bot=MagicMock(), update=update)
telegram._status_table(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
telegram._status_table(bot=MagicMock(), update=update)
telegram._status_table(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
@ -294,7 +295,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
# Create some test data
freqtradebot.create_trades()
telegram._status_table(bot=MagicMock(), update=update)
telegram._status_table(update=update, context=MagicMock())
text = re.sub('</?pre>', '', msg_mock.call_args_list[-1][0][0])
line = text.split("\n")
@ -346,8 +347,10 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
trade.is_open = False
# Try valid data
update.message.text = '/daily 2'
telegram._daily(bot=MagicMock(), update=update)
# /daily 2
context = MagicMock()
context.args = ["2"]
telegram._daily(update=update, context=context)
assert msg_mock.call_count == 1
assert 'Daily' in msg_mock.call_args_list[0][0][0]
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
@ -369,9 +372,10 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
trade.close_date = datetime.utcnow()
trade.is_open = False
update.message.text = '/daily 1'
telegram._daily(bot=MagicMock(), update=update)
# /daily 1
context = MagicMock()
context.args = ["1"]
telegram._daily(update=update, context=context)
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
@ -398,16 +402,20 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
# Try invalid data
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
update.message.text = '/daily -2'
telegram._daily(bot=MagicMock(), update=update)
# /daily -2
context = MagicMock()
context.args = ["-2"]
telegram._daily(update=update, context=context)
assert msg_mock.call_count == 1
assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0]
# Try invalid data
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
update.message.text = '/daily today'
telegram._daily(bot=MagicMock(), update=update)
# /daily today
context = MagicMock()
context.args = ["today"]
telegram._daily(update=update, context=context)
assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0]
@ -433,7 +441,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot)
telegram._profit(bot=MagicMock(), update=update)
telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
@ -445,7 +453,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
telegram._profit(bot=MagicMock(), update=update)
telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock()
@ -457,7 +465,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
trade.close_date = datetime.utcnow()
trade.is_open = False
telegram._profit(bot=MagicMock(), update=update)
telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
@ -507,7 +515,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> N
telegram = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update)
telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1
assert '*BTC:*' in result
@ -536,7 +544,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot)
freqtradebot.config['dry_run'] = False
telegram._balance(bot=MagicMock(), update=update)
telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1
assert 'All balances are zero.' in result
@ -557,7 +565,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None
telegram = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update)
telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1
assert "Running in Dry Run, balances are not available." in result
@ -593,7 +601,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None
telegram = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update)
telegram._balance(update=update, context=MagicMock())
assert msg_mock.call_count > 1
# Test if wrap happens around 4000 -
# and each single currency-output is around 120 characters long so we need
@ -615,7 +623,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
freqtradebot.state = State.STOPPED
assert freqtradebot.state == State.STOPPED
telegram._start(bot=MagicMock(), update=update)
telegram._start(update=update, context=MagicMock())
assert freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 1
@ -633,7 +641,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
freqtradebot.state = State.RUNNING
assert freqtradebot.state == State.RUNNING
telegram._start(bot=MagicMock(), update=update)
telegram._start(update=update, context=MagicMock())
assert freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 1
assert 'already running' in msg_mock.call_args_list[0][0][0]
@ -652,7 +660,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
freqtradebot.state = State.RUNNING
assert freqtradebot.state == State.RUNNING
telegram._stop(bot=MagicMock(), update=update)
telegram._stop(update=update, context=MagicMock())
assert freqtradebot.state == State.STOPPED
assert msg_mock.call_count == 1
assert 'stopping trader' in msg_mock.call_args_list[0][0][0]
@ -671,7 +679,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
freqtradebot.state = State.STOPPED
assert freqtradebot.state == State.STOPPED
telegram._stop(bot=MagicMock(), update=update)
telegram._stop(update=update, context=MagicMock())
assert freqtradebot.state == State.STOPPED
assert msg_mock.call_count == 1
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
@ -689,7 +697,7 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot)
assert freqtradebot.config['max_open_trades'] != 0
telegram._stopbuy(bot=MagicMock(), update=update)
telegram._stopbuy(update=update, context=MagicMock())
assert freqtradebot.config['max_open_trades'] == 0
assert msg_mock.call_count == 1
assert 'No more buy will occur from now. Run /reload_conf to reset.' \
@ -709,7 +717,7 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
freqtradebot.state = State.RUNNING
assert freqtradebot.state == State.RUNNING
telegram._reload_conf(bot=MagicMock(), update=update)
telegram._reload_conf(update=update, context=MagicMock())
assert freqtradebot.state == State.RELOAD_CONF
assert msg_mock.call_count == 1
assert 'reloading config' in msg_mock.call_args_list[0][0][0]
@ -742,8 +750,10 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
# Increase the price and sell it
mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up)
update.message.text = '/forcesell 1'
telegram._forcesell(bot=MagicMock(), update=update)
# /forcesell 1
context = MagicMock()
context.args = ["1"]
telegram._forcesell(update=update, context=context)
assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0]
@ -796,8 +806,10 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
trade = Trade.query.first()
assert trade
update.message.text = '/forcesell 1'
telegram._forcesell(bot=MagicMock(), update=update)
# /forcesell 1
context = MagicMock()
context.args = ["1"]
telegram._forcesell(update=update, context=context)
assert rpc_mock.call_count == 2
@ -842,8 +854,10 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
freqtradebot.create_trades()
rpc_mock.reset_mock()
update.message.text = '/forcesell all'
telegram._forcesell(bot=MagicMock(), update=update)
# /forcesell all
context = MagicMock()
context.args = ["all"]
telegram._forcesell(update=update, context=context)
assert rpc_mock.call_count == 4
msg = rpc_mock.call_args_list[0][0][0]
@ -882,24 +896,29 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
# Trader is not running
freqtradebot.state = State.STOPPED
update.message.text = '/forcesell 1'
telegram._forcesell(bot=MagicMock(), update=update)
# /forcesell 1
context = MagicMock()
context.args = ["1"]
telegram._forcesell(update=update, context=context)
assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0]
# No argument
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
update.message.text = '/forcesell'
telegram._forcesell(bot=MagicMock(), update=update)
context = MagicMock()
context.args = []
telegram._forcesell(update=update, context=context)
assert msg_mock.call_count == 1
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
# Invalid argument
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
update.message.text = '/forcesell 123456'
telegram._forcesell(bot=MagicMock(), update=update)
# /forcesell 123456
context = MagicMock()
context.args = ["123456"]
telegram._forcesell(update=update, context=context)
assert msg_mock.call_count == 1
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
@ -921,8 +940,10 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None:
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot)
update.message.text = '/forcebuy ETH/BTC'
telegram._forcebuy(bot=MagicMock(), update=update)
# /forcebuy ETH/BTC
context = MagicMock()
context.args = ["ETH/BTC"]
telegram._forcebuy(update=update, context=context)
assert fbuy_mock.call_count == 1
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
@ -931,8 +952,10 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None:
# Reset and retry with specified price
fbuy_mock = MagicMock(return_value=None)
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
update.message.text = '/forcebuy ETH/BTC 0.055'
telegram._forcebuy(bot=MagicMock(), update=update)
# /forcebuy ETH/BTC 0.055
context = MagicMock()
context.args = ["ETH/BTC", "0.055"]
telegram._forcebuy(update=update, context=context)
assert fbuy_mock.call_count == 1
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
@ -955,7 +978,7 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non
telegram = Telegram(freqtradebot)
update.message.text = '/forcebuy ETH/Nonepair'
telegram._forcebuy(bot=MagicMock(), update=update)
telegram._forcebuy(update=update, context=MagicMock())
assert rpc_mock.call_count == 1
assert rpc_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.'
@ -995,7 +1018,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
trade.close_date = datetime.utcnow()
trade.is_open = False
telegram._performance(bot=MagicMock(), update=update)
telegram._performance(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>ETH/BTC\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
@ -1021,7 +1044,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
telegram._count(bot=MagicMock(), update=update)
telegram._count(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
@ -1030,7 +1053,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
# Create some test data
freqtradebot.create_trades()
msg_mock.reset_mock()
telegram._count(bot=MagicMock(), update=update)
telegram._count(update=update, context=MagicMock())
msg = '<pre> current max total stake\n--------- ----- -------------\n' \
' 1 {} {}</pre>'\
@ -1052,7 +1075,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot)
telegram._whitelist(bot=MagicMock(), update=update)
telegram._whitelist(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert ('Using whitelist `StaticPairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`'
in msg_mock.call_args_list[0][0][0])
@ -1073,7 +1096,7 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot)
telegram._whitelist(bot=MagicMock(), update=update)
telegram._whitelist(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert ('Using whitelist `VolumePairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`'
in msg_mock.call_args_list[0][0][0])
@ -1090,13 +1113,17 @@ def test_blacklist_static(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot)
telegram._blacklist(bot=MagicMock(), update=update, args=[])
telegram._blacklist(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`"
in msg_mock.call_args_list[0][0][0])
msg_mock.reset_mock()
telegram._blacklist(bot=MagicMock(), update=update, args=["ETH/BTC"])
# /blacklist ETH/BTC
context = MagicMock()
context.args = ["ETH/BTC"]
telegram._blacklist(update=update, context=context)
assert msg_mock.call_count == 1
assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`"
in msg_mock.call_args_list[0][0][0])
@ -1115,7 +1142,7 @@ def test_edge_disabled(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot)
telegram._edge(bot=MagicMock(), update=update)
telegram._edge(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0]
@ -1137,7 +1164,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot)
telegram._edge(bot=MagicMock(), update=update)
telegram._edge(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '<b>Edge only validated following pairs:</b>\n<pre>' in msg_mock.call_args_list[0][0][0]
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
@ -1154,7 +1181,7 @@ def test_help_handle(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot)
telegram._help(bot=MagicMock(), update=update)
telegram._help(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
@ -1169,7 +1196,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram._version(bot=MagicMock(), update=update)
telegram._version(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
@ -1395,9 +1422,11 @@ def test__send_msg(default_conf, mocker) -> None:
bot = MagicMock()
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram._updater = MagicMock()
telegram._updater.bot = bot
telegram._config['telegram']['enabled'] = True
telegram._send_msg('test', bot)
telegram._send_msg('test')
assert len(bot.method_calls) == 1
@ -1407,9 +1436,11 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram._updater = MagicMock()
telegram._updater.bot = bot
telegram._config['telegram']['enabled'] = True
telegram._send_msg('test', bot)
telegram._send_msg('test')
# Bot should've tried to send it twice
assert len(bot.method_calls) == 2

View File

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

View File

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

View File

@ -1152,7 +1152,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
side_effect=DependencyException()
)
freqtrade.handle_stoploss_on_exchange(trade)
assert log_has('Unable to place a stoploss order on exchange: ', caplog)
assert log_has('Unable to place a stoploss order on exchange.', caplog)
assert trade.stoploss_order_id is None
# Fifth case: get_order returns InvalidOrder
@ -1200,6 +1200,50 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
assert trade.is_open is True
def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
markets, limit_buy_order, limit_sell_order):
rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker)
sell_mock = MagicMock(return_value={'id': limit_sell_order['id']})
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=sell_mock,
get_fee=fee,
markets=PropertyMock(return_value=markets),
get_order=MagicMock(return_value={'status': 'canceled'}),
stoploss_limit=MagicMock(side_effect=InvalidOrderException()),
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
freqtrade.create_trades()
trade = Trade.query.first()
caplog.clear()
freqtrade.create_stoploss_order(trade, 200, 199)
assert trade.stoploss_order_id is None
assert trade.sell_reason == SellType.EMERGENCY_SELL.value
assert log_has("Unable to place a stoploss order on exchange. ", caplog)
assert log_has("Selling the trade forcefully", caplog)
# Should call a market sell
assert sell_mock.call_count == 1
assert sell_mock.call_args[1]['ordertype'] == 'market'
assert sell_mock.call_args[1]['pair'] == trade.pair
assert sell_mock.call_args[1]['amount'] == trade.amount
# Rpc is sending first buy, then sell
assert rpc_mock.call_count == 2
assert rpc_mock.call_args_list[1][0][0]['sell_reason'] == SellType.EMERGENCY_SELL.value
assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market'
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
markets, limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set

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

View File

@ -4,8 +4,10 @@ from pathlib import Path
from unittest.mock import MagicMock
import plotly.graph_objects as go
import pytest
from plotly.subplots import make_subplots
from freqtrade import OperationalException
from freqtrade.configuration import TimeRange
from freqtrade.data import history
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
@ -335,6 +337,15 @@ def test_start_plot_profit(mocker):
assert called_config['pairs'] == ["ETH/BTC"]
def test_start_plot_profit_error(mocker):
args = [
"plot-profit",
"--pairs", "ETH/BTC"
]
with pytest.raises(OperationalException):
start_plot_profit(get_args(args))
def test_plot_profit(default_conf, mocker, caplog):
default_conf['trade_source'] = 'file'
default_conf["datadir"] = history.make_testdata_path(None)

View File

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