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