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