Merge pull request #2872 from freqtrade/interface_ordertimeoutcallback

Buy order timeout callback
This commit is contained in:
hroff-1902 2020-04-25 19:02:15 +03:00 committed by GitHub
commit 485e324d36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 464 additions and 59 deletions

91
docs/strategy-advanced.md Normal file
View File

@ -0,0 +1,91 @@
# Advanced Strategies
This page explains some advanced concepts available for strategies.
If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation first.
## Custom order timeout rules
Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
However, freqtrade also offers a custom callback for both ordertypes, which allows you to decide based on custom criteria if a order did time out or not.
!!! Note
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
### Custom order timeout example
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
The function must return either `True` (cancel order) or `False` (keep order alive).
``` python
from datetime import datetime, timestamp
from freqtrade.persistence import Trade
class Awesomestrategy(IStrategy):
# ... populate_* methods
# Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
unfilledtimeout = {
'buy': 60 * 25,
'sell': 60 * 25
}
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5):
return True
elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3):
return True
elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24):
return True
return False
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5):
return True
elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3):
return True
elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24):
return True
return False
```
!!! Note
For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first.
### Custom order timeout example (using additional data)
``` python
from datetime import datetime, timestamp
from freqtrade.persistence import Trade
class Awesomestrategy(IStrategy):
# ... populate_* methods
# Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
unfilledtimeout = {
'buy': 60 * 25,
'sell': 60 * 25
}
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1)
current_price = ob['bids'][0][0]
# Cancel buy order if price is more than 2% above the order.
if current_price > order['price'] * 1.02:
return True
return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1)
current_price = ob['asks'][0][0]
# Cancel sell order if price is more than 2% below the order.
if current_price < order['price'] * 0.98:
return True
return False
```

View File

@ -1,7 +1,6 @@
# Strategy Customization # Strategy Customization
This page explains where to customize your strategies, and add new This page explains where to customize your strategies, and add new indicators.
indicators.
## Install a custom strategy file ## Install a custom strategy file

View File

@ -77,7 +77,7 @@ Results will be located in `user_data/strategies/<strategyclassname>.py`.
``` output ``` output
usage: freqtrade new-strategy [-h] [--userdir PATH] [-s NAME] usage: freqtrade new-strategy [-h] [--userdir PATH] [-s NAME]
[--template {full,minimal}] [--template {full,minimal,advanced}]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -86,10 +86,10 @@ optional arguments:
-s NAME, --strategy NAME -s NAME, --strategy NAME
Specify strategy class name which will be used by the Specify strategy class name which will be used by the
bot. bot.
--template {full,minimal} --template {full,minimal,advanced}
Use a template which is either `minimal` or `full` Use a template which is either `minimal`, `full`
(containing multiple sample indicators). Default: (containing multiple sample indicators) or `advanced`.
`full`. Default: `full`.
``` ```
@ -105,6 +105,12 @@ With custom user directory
freqtrade new-strategy --userdir ~/.freqtrade/ --strategy AwesomeStrategy freqtrade new-strategy --userdir ~/.freqtrade/ --strategy AwesomeStrategy
``` ```
Using the advanced template (populates all optional functions and methods)
```bash
freqtrade new-strategy --strategy AwesomeStrategy --template advanced
```
## Create new hyperopt ## Create new hyperopt
Creates a new hyperopt from a template similar to SampleHyperopt. Creates a new hyperopt from a template similar to SampleHyperopt.
@ -114,7 +120,7 @@ Results will be located in `user_data/hyperopts/<classname>.py`.
``` output ``` output
usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME] usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME]
[--template {full,minimal}] [--template {full,minimal,advanced}]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -122,10 +128,10 @@ optional arguments:
Path to userdata directory. Path to userdata directory.
--hyperopt NAME Specify hyperopt class name which will be used by the --hyperopt NAME Specify hyperopt class name which will be used by the
bot. bot.
--template {full,minimal} --template {full,minimal,advanced}
Use a template which is either `minimal` or `full` Use a template which is either `minimal`, `full`
(containing multiple sample indicators). Default: (containing multiple sample indicators) or `advanced`.
`full`. Default: `full`.
``` ```
### Sample usage of new-hyperopt ### Sample usage of new-hyperopt

View File

@ -387,9 +387,9 @@ AVAILABLE_CLI_OPTIONS = {
# Templating options # Templating options
"template": Arg( "template": Arg(
'--template', '--template',
help='Use a template which is either `minimal` or ' help='Use a template which is either `minimal`, '
'`full` (containing multiple sample indicators). Default: `%(default)s`.', '`full` (containing multiple sample indicators) or `advanced`. Default: `%(default)s`.',
choices=['full', 'minimal'], choices=['full', 'minimal', 'advanced'],
default='full', default='full',
), ),
# Plot dataframe # Plot dataframe

View File

@ -8,7 +8,7 @@ from freqtrade.configuration.directory_operations import (copy_sample_files,
create_userdata_dir) create_userdata_dir)
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import render_template from freqtrade.misc import render_template, render_template_with_fallback
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -32,10 +32,27 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
""" """
Deploy new strategy from template to strategy_path Deploy new strategy from template to strategy_path
""" """
indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",) fallback = 'full'
buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",) indicators = render_template_with_fallback(
sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",) templatefile=f"subtemplates/indicators_{subtemplate}.j2",
plot_config = render_template(templatefile=f"subtemplates/plot_config_{subtemplate}.j2",) templatefallbackfile=f"subtemplates/indicators_{fallback}.j2",
)
buy_trend = render_template_with_fallback(
templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/buy_trend_{fallback}.j2",
)
sell_trend = render_template_with_fallback(
templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/sell_trend_{fallback}.j2",
)
plot_config = render_template_with_fallback(
templatefile=f"subtemplates/plot_config_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/plot_config_{fallback}.j2",
)
additional_methods = render_template_with_fallback(
templatefile=f"subtemplates/strategy_methods_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/strategy_methods_empty.j2",
)
strategy_text = render_template(templatefile='base_strategy.py.j2', strategy_text = render_template(templatefile='base_strategy.py.j2',
arguments={"strategy": strategy_name, arguments={"strategy": strategy_name,
@ -43,6 +60,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
"buy_trend": buy_trend, "buy_trend": buy_trend,
"sell_trend": sell_trend, "sell_trend": sell_trend,
"plot_config": plot_config, "plot_config": plot_config,
"additional_methods": additional_methods,
}) })
logger.info(f"Writing strategy to `{strategy_path}`.") logger.info(f"Writing strategy to `{strategy_path}`.")
@ -73,14 +91,23 @@ def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: st
""" """
Deploys a new hyperopt template to hyperopt_path Deploys a new hyperopt template to hyperopt_path
""" """
buy_guards = render_template( fallback = 'full'
templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",) buy_guards = render_template_with_fallback(
sell_guards = render_template( templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",
templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",) templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2",
buy_space = render_template( )
templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",) sell_guards = render_template_with_fallback(
sell_space = render_template( templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",
templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",) templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2",
)
buy_space = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2",
)
sell_space = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2",
)
strategy_text = render_template(templatefile='base_hyperopt.py.j2', strategy_text = render_template(templatefile='base_hyperopt.py.j2',
arguments={"hyperopt": hyperopt_name, arguments={"hyperopt": hyperopt_name,

View File

@ -35,3 +35,10 @@ class TemporaryError(FreqtradeException):
This could happen when an exchange is congested, unavailable, or the user This could happen when an exchange is congested, unavailable, or the user
has networking problems. Usually resolves itself after a time. has networking problems. Usually resolves itself after a time.
""" """
class StrategyError(FreqtradeException):
"""
Errors with custom user-code deteced.
Usually caused by errors in the strategy.
"""

View File

@ -27,6 +27,7 @@ from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.strategy.interface import IStrategy, SellType
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -869,7 +870,12 @@ class FreqtradeBot:
if (order['side'] == 'buy' and ( if (order['side'] == 'buy' and (
trade_state_update trade_state_update
or self._check_timed_out('buy', order))): or self._check_timed_out('buy', order)
or strategy_safe_wrapper(self.strategy.check_buy_timeout,
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
self.handle_timedout_limit_buy(trade, order) self.handle_timedout_limit_buy(trade, order)
self.wallets.update() self.wallets.update()
order_type = self.strategy.order_types['buy'] order_type = self.strategy.order_types['buy']
@ -877,7 +883,11 @@ class FreqtradeBot:
elif (order['side'] == 'sell' and ( elif (order['side'] == 'sell' and (
trade_state_update trade_state_update
or self._check_timed_out('sell', order))): or self._check_timed_out('sell', order)
or strategy_safe_wrapper(self.strategy.check_sell_timeout,
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
reason = self.handle_timedout_limit_sell(trade, order) reason = self.handle_timedout_limit_sell(trade, order)
self.wallets.update() self.wallets.update()
order_type = self.strategy.order_types['sell'] order_type = self.strategy.order_types['sell']

View File

@ -163,3 +163,15 @@ def render_template(templatefile: str, arguments: dict = {}) -> str:
) )
template = env.get_template(templatefile) template = env.get_template(templatefile)
return template.render(**arguments) return template.render(**arguments)
def render_template_with_fallback(templatefile: str, templatefallbackfile: str,
arguments: dict = {}) -> str:
"""
Use templatefile if possible, otherwise fall back to templatefallbackfile
"""
from jinja2.exceptions import TemplateNotFound
try:
return render_template(templatefile, arguments)
except TemplateNotFound:
return render_template(templatefallbackfile, arguments)

View File

@ -3,21 +3,21 @@ IStrategy interface
This module defines the interface to apply for strategies This module defines the interface to apply for strategies
""" """
import logging import logging
import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Tuple from typing import Dict, List, NamedTuple, Optional, Tuple
import warnings
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import StrategyError
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
from freqtrade.exceptions import DependencyException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -149,6 +149,42 @@ class IStrategy(ABC):
:return: DataFrame with sell column :return: DataFrame with sell column
""" """
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
"""
Check buy timeout function callback.
This method can be used to override the buy-timeout.
It is called whenever a limit buy order has been created,
and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough.
When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for
:param trade: trade object.
:param order: Order dictionary as returned from CCXT.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is cancelled.
"""
return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
"""
Check sell timeout function callback.
This method can be used to override the sell-timeout.
It is called whenever a limit sell order has been created,
and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough.
When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for
:param trade: trade object.
:param order: Order dictionary as returned from CCXT.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is cancelled.
"""
return False
def informative_pairs(self) -> List[Tuple[str, str]]: def informative_pairs(self) -> List[Tuple[str, str]]:
""" """
Define additional, informative pair/interval combinations to be cached from the exchange. Define additional, informative pair/interval combinations to be cached from the exchange.
@ -258,8 +294,7 @@ class IStrategy(ABC):
elif df_date != dataframe["date"].iloc[-1]: elif df_date != dataframe["date"].iloc[-1]:
message = "last date" message = "last date"
if message: if message:
raise DependencyException("Dataframe returned from strategy has mismatching " raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
f"{message}.")
def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]:
""" """
@ -276,22 +311,13 @@ class IStrategy(ABC):
latest_date = dataframe['date'].max() latest_date = dataframe['date'].max()
try: try:
df_len, df_close, df_date = self.preserve_df(dataframe) df_len, df_close, df_date = self.preserve_df(dataframe)
dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) dataframe = strategy_safe_wrapper(
self._analyze_ticker_internal, message=""
)(dataframe, {'pair': pair})
self.assert_df(dataframe, df_len, df_close, df_date) self.assert_df(dataframe, df_len, df_close, df_date)
except ValueError as error: except StrategyError as error:
logger.warning('Unable to analyze candle (OHLCV) data for pair %s: %s', logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}")
pair, str(error))
return False, False
except DependencyException as error:
logger.warning("Unable to analyze candle (OHLCV) data for pair %s: %s",
pair, str(error))
return False, False
except Exception as error:
logger.exception(
'Unexpected error when analyzing candle (OHLCV) data for pair %s: %s',
pair,
str(error)
)
return False, False return False, False
if dataframe.empty: if dataframe.empty:

View File

@ -0,0 +1,35 @@
import logging
from freqtrade.exceptions import StrategyError
logger = logging.getLogger(__name__)
def strategy_safe_wrapper(f, message: str = "", default_retval=None):
"""
Wrapper around user-provided methods and functions.
Caches all exceptions and returns either the default_retval (if it's not None) or raises
a StrategyError exception, which then needs to be handled by the calling method.
"""
def wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
except ValueError as error:
logger.warning(
f"{message}"
f"Strategy caused the following exception: {error}"
f"{f}"
)
if default_retval is None:
raise StrategyError(str(error)) from error
return default_retval
except Exception as error:
logger.exception(
f"{message}"
f"Unexpected error {error} calling {f}"
)
if default_retval is None:
raise StrategyError(str(error)) from error
return default_retval
return wrapper

View File

@ -137,3 +137,4 @@ class {{ strategy }}(IStrategy):
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
{{ additional_methods | indent(4) }}

View File

@ -0,0 +1,40 @@
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
"""
Check buy timeout function callback.
This method can be used to override the buy-timeout.
It is called whenever a limit buy order has been created,
and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for
:param trade: trade object.
:param order: Order dictionary as returned from CCXT.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is cancelled.
"""
return False
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
"""
Check sell timeout function callback.
This method can be used to override the sell-timeout.
It is called whenever a limit sell order has been created,
and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for
:param trade: trade object.
:param order: Order dictionary as returned from CCXT.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is cancelled.
"""
return False

View File

@ -24,6 +24,7 @@ nav:
- Plotting: plotting.md - Plotting: plotting.md
- SQL Cheatsheet: sql_cheatsheet.md - SQL Cheatsheet: sql_cheatsheet.md
- Advanced Post-installation Tasks: advanced-setup.md - Advanced Post-installation Tasks: advanced-setup.md
- Advanced Strategy: strategy-advanced.md
- Advanced Hyperopt: advanced-hyperopt.md - Advanced Hyperopt: advanced-hyperopt.md
- Sandbox Testing: sandbox-testing.md - Sandbox Testing: sandbox-testing.md
- Deprecated Features: deprecated.md - Deprecated Features: deprecated.md

View File

@ -9,10 +9,11 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import StrategyError
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from tests.conftest import get_patched_exchange, log_has from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from tests.conftest import get_patched_exchange, log_has, log_has_re
from .strats.default_strategy import DefaultStrategy from .strats.default_strategy import DefaultStrategy
@ -71,7 +72,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_his
) )
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
ohlcv_history) ohlcv_history)
assert log_has('Unable to analyze candle (OHLCV) data for pair foo: xyz', caplog) assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history): def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history):
@ -121,7 +122,7 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'assert_df', _STRATEGY, 'assert_df',
side_effect=DependencyException('Dataframe returned...') side_effect=StrategyError('Dataframe returned...')
) )
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ohlcv_history) ohlcv_history)
@ -134,15 +135,15 @@ def test_assert_df(default_conf, mocker, ohlcv_history):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
with pytest.raises(DependencyException, match=r"Dataframe returned from strategy.*length\."): with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*length\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1, _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1,
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
with pytest.raises(DependencyException, with pytest.raises(StrategyError,
match=r"Dataframe returned from strategy.*last close price\."): match=r"Dataframe returned from strategy.*last close price\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date']) ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date'])
with pytest.raises(DependencyException, with pytest.raises(StrategyError,
match=r"Dataframe returned from strategy.*last date\."): match=r"Dataframe returned from strategy.*last date\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date']) ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date'])
@ -389,3 +390,38 @@ def test_is_pair_locked(default_conf):
pair = 'ETH/BTC' pair = 'ETH/BTC'
strategy.unlock_pair(pair) strategy.unlock_pair(pair)
assert not strategy.is_pair_locked(pair) assert not strategy.is_pair_locked(pair)
@pytest.mark.parametrize('error', [
ValueError, KeyError, Exception,
])
def test_strategy_safe_wrapper_error(caplog, error):
def failing_method():
raise error('This is an error.')
def working_method(argumentpassedin):
return argumentpassedin
with pytest.raises(StrategyError, match=r'This is an error.'):
strategy_safe_wrapper(failing_method, message='DeadBeef')()
assert log_has_re(r'DeadBeef.*', caplog)
ret = strategy_safe_wrapper(failing_method, message='DeadBeef', default_retval=True)()
assert isinstance(ret, bool)
assert ret
@pytest.mark.parametrize('value', [
1, 22, 55, True, False, {'a': 1, 'b': '112'},
[1, 2, 3, 4], (4, 2, 3, 6)
])
def test_strategy_safe_wrapper(value):
def working_method(argumentpassedin):
return argumentpassedin
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value)
assert type(ret) == type(value)
assert ret == value

View File

@ -1939,6 +1939,53 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
freqtrade.handle_trade(trade) freqtrade.handle_trade(trade)
def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_order_old, open_trade,
fee, mocker) -> None:
default_conf["unfilledtimeout"] = {"buy": 1400, "sell": 30}
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old),
cancel_order=cancel_order_mock,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf)
Trade.session.add(open_trade)
# Return false - trade remains open
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 1
assert freqtrade.strategy.check_buy_timeout.call_count == 1
# Raise Keyerror ... (no impact on trade)
freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 1
assert freqtrade.strategy.check_buy_timeout.call_count == 1
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
# Trade should be closed since the function returns true
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 0
assert freqtrade.strategy.check_buy_timeout.call_count == 1
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade, def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade,
fee, mocker) -> None: fee, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
@ -1955,6 +2002,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
Trade.session.add(open_trade) Trade.session.add(open_trade)
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
@ -1962,6 +2010,8 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades) nb_trades = len(trades)
assert nb_trades == 0 assert nb_trades == 0
# Custom user buy-timeout is never called
assert freqtrade.strategy.check_buy_timeout.call_count == 0
def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, open_trade, def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, open_trade,
@ -2018,6 +2068,51 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord
assert nb_trades == 1 assert nb_trades == 1
def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_order_old, mocker,
open_trade) -> None:
default_conf["unfilledtimeout"] = {"buy": 1440, "sell": 1440}
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock()
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf)
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
open_trade.is_open = False
Trade.session.add(open_trade)
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
# Return false - No impact
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 0
assert open_trade.is_open is False
assert freqtrade.strategy.check_sell_timeout.call_count == 1
freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError)
# Return Error - No impact
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 0
assert open_trade.is_open is False
assert freqtrade.strategy.check_sell_timeout.call_count == 1
# Return True - sells!
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert open_trade.is_open is True
assert freqtrade.strategy.check_sell_timeout.call_count == 1
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker, def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker,
open_trade) -> None: open_trade) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
@ -2037,11 +2132,14 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
Trade.session.add(open_trade) Trade.session.add(open_trade)
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
# check it does cancel sell orders over the time limit # check it does cancel sell orders over the time limit
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1 assert rpc_mock.call_count == 1
assert open_trade.is_open is True assert open_trade.is_open is True
# Custom user sell-timeout is never called
assert freqtrade.strategy.check_sell_timeout.call_count == 0
def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade, def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade,

View File

@ -9,7 +9,9 @@ import pytest
from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
file_load_json, format_ms_time, pair_to_filename, file_load_json, format_ms_time, pair_to_filename,
plural, safe_value_fallback, shorten_date) plural, render_template,
render_template_with_fallback, safe_value_fallback,
shorten_date)
def test_shorten_date() -> None: def test_shorten_date() -> None:
@ -144,3 +146,17 @@ def test_plural() -> None:
assert plural(1.5, "ox", "oxen") == "oxen" assert plural(1.5, "ox", "oxen") == "oxen"
assert plural(-0.5, "ox", "oxen") == "oxen" assert plural(-0.5, "ox", "oxen") == "oxen"
assert plural(-1.5, "ox", "oxen") == "oxen" assert plural(-1.5, "ox", "oxen") == "oxen"
def test_render_template_fallback(mocker):
from jinja2.exceptions import TemplateNotFound
with pytest.raises(TemplateNotFound):
val = render_template(
templatefile='subtemplates/indicators_does-not-exist.j2',)
val = render_template_with_fallback(
templatefile='subtemplates/indicators_does-not-exist.j2',
templatefallbackfile='subtemplates/indicators_minimal.j2',
)
assert isinstance(val, str)
assert 'if self.dp' in val