Merge branch 'develop' into plot_commands

This commit is contained in:
Matthias 2019-08-30 20:42:58 +02:00
commit 816d942ded
39 changed files with 494 additions and 326 deletions

View File

@ -79,18 +79,18 @@ freqtrade backtesting --datadir user_data/data/bittrex-20180101
#### With a (custom) strategy file #### With a (custom) strategy file
```bash ```bash
freqtrade -s TestStrategy backtesting freqtrade -s SampleStrategy backtesting
``` ```
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory. Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory.
#### Comparing multiple Strategies #### Comparing multiple Strategies
```bash ```bash
freqtrade backtesting --strategy-list TestStrategy1 AwesomeStrategy --ticker-interval 5m freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --ticker-interval 5m
``` ```
Where `TestStrategy1` and `AwesomeStrategy` refer to class names of strategies. Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies.
#### Exporting trades to file #### Exporting trades to file
@ -103,7 +103,7 @@ The exported trades can be used for [further analysis](#further-backtest-result-
#### Exporting trades to file specifying a custom filename #### Exporting trades to file specifying a custom filename
```bash ```bash
freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json
``` ```
#### Running backtest with smaller testset #### Running backtest with smaller testset

View File

@ -61,8 +61,9 @@ Mandatory parameters are marked as **Required**.
| `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). | `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.***
| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.***
| `exchange.password` | '' | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.***
| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). | `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)).
| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). | `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)).
| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
@ -76,8 +77,8 @@ Mandatory parameters are marked as **Required**.
| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists). | `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists).
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). | `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.***
| `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.***
| `webhook.enabled` | false | Enable usage of Webhook notifications | `webhook.enabled` | false | Enable usage of Webhook notifications
| `webhook.url` | false | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. | `webhook.url` | false | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
| `webhook.webhookbuy` | false | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhookbuy` | false | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.

View File

@ -139,7 +139,7 @@ You can override strategy settings as demonstrated below.
# Define some constants # Define some constants
ticker_interval = "5m" ticker_interval = "5m"
# Name of the strategy class # Name of the strategy class
strategy_name = 'TestStrategy' strategy_name = 'SampleStrategy'
# Path to user data # Path to user data
user_data_dir = 'user_data' user_data_dir = 'user_data'
# Location of the strategy # Location of the strategy

View File

@ -1 +1 @@
mkdocs-material==4.4.0 mkdocs-material==4.4.1

View File

@ -24,7 +24,7 @@ strategy file will be updated on Github. Put your custom strategy file
into the directory `user_data/strategies`. into the directory `user_data/strategies`.
Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes.
`cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py` `cp user_data/strategies/sample_strategy.py user_data/strategies/awesome-strategy.py`
### Anatomy of a strategy ### Anatomy of a strategy
@ -36,14 +36,19 @@ A strategy file contains all the information needed to build a good strategy:
- Minimal ROI recommended - Minimal ROI recommended
- Stoploss strongly recommended - Stoploss strongly recommended
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`. The bot also include a sample strategy called `SampleStrategy` you can update: `user_data/strategies/sample_strategy.py`.
You can test it with the parameter: `--strategy TestStrategy` You can test it with the parameter: `--strategy SampleStrategy`
Additionally, there is an attribute called `INTERFACE_VERSION`, which defines the version of the strategy interface the bot should use.
The current version is 2 - which is also the default when it's not set explicitly in the strategy.
Future versions will require this to be set.
```bash ```bash
freqtrade --strategy AwesomeStrategy freqtrade --strategy AwesomeStrategy
``` ```
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py) **For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py)
file as reference.** file as reference.**
!!! Note Strategies and Backtesting !!! Note Strategies and Backtesting
@ -109,9 +114,8 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
return dataframe return dataframe
``` ```
!!! Note "Want more indicator examples?" !!! Note "Want more indicator examples?"
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).<br/> Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py).
Then uncomment indicators you need. Then uncomment indicators you need.
### Buy signal rules ### Buy signal rules
@ -122,7 +126,7 @@ It's important to always return the dataframe without removing/modifying the col
This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action". This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action".
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/sample_strategy.py`:
```python ```python
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@ -152,7 +156,7 @@ It's important to always return the dataframe without removing/modifying the col
This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action". This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action".
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/sample_strategy.py`:
```python ```python
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@ -11,7 +11,7 @@ class DependencyException(Exception):
class OperationalException(Exception): class OperationalException(Exception):
""" """
Requires manual intervention. Requires manual intervention and will usually stop the bot.
This happens when an exchange returns an unexpected error during runtime This happens when an exchange returns an unexpected error during runtime
or given configuration is invalid. or given configuration is invalid.
""" """

View File

@ -4,6 +4,7 @@ This module contains the configuration class
import logging import logging
import warnings import warnings
from argparse import Namespace from argparse import Namespace
from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, List, Optional from typing import Any, Callable, Dict, List, Optional
@ -56,7 +57,7 @@ class Configuration(object):
config: Dict[str, Any] = {} config: Dict[str, Any] = {}
if not files: if not files:
return constants.MINIMAL_CONFIG.copy() return deepcopy(constants.MINIMAL_CONFIG)
# We expect here a list of config filenames # We expect here a list of config filenames
for path in files: for path in files:
@ -160,6 +161,11 @@ class Configuration(object):
Extract information for sys.argv and load directory configurations Extract information for sys.argv and load directory configurations
--user-data, --datadir --user-data, --datadir
""" """
# Check exchange parameter here - otherwise `datadir` might be wrong.
if "exchange" in self.args and self.args.exchange:
config['exchange']['name'] = self.args.exchange
logger.info(f"Using exchange {config['exchange']['name']}")
if 'user_data_dir' in self.args and self.args.user_data_dir: if 'user_data_dir' in self.args and self.args.user_data_dir:
config.update({'user_data_dir': self.args.user_data_dir}) config.update({'user_data_dir': self.args.user_data_dir})
elif 'user_data_dir' not in config: elif 'user_data_dir' not in config:
@ -297,10 +303,6 @@ class Configuration(object):
self._args_to_config(config, argname='days', self._args_to_config(config, argname='days',
logstring='Detected --days: {}') logstring='Detected --days: {}')
if "exchange" in self.args and self.args.exchange:
config['exchange']['name'] = self.args.exchange
logger.info(f"Using exchange {config['exchange']['name']}")
def _process_runmode(self, config: Dict[str, Any]) -> None: def _process_runmode(self, config: Dict[str, Any]) -> None:
if not self.runmode: if not self.runmode:
@ -361,7 +363,7 @@ class Configuration(object):
config['pairs'] = config.get('exchange', {}).get('pair_whitelist') config['pairs'] = config.get('exchange', {}).get('pair_whitelist')
else: else:
# Fall back to /dl_path/pairs.json # Fall back to /dl_path/pairs.json
pairs_file = Path(config['datadir']) / config['exchange']['name'].lower() / "pairs.json" pairs_file = Path(config['datadir']) / "pairs.json"
if pairs_file.exists(): if pairs_file.exists():
with pairs_file.open('r') as f: with pairs_file.open('r') as f:
config['pairs'] = json_load(f) config['pairs'] = json_load(f)

View File

@ -280,6 +280,35 @@ def download_pair_history(datadir: Optional[Path],
return False return False
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
dl_path: Path, timerange: TimeRange,
erase=False) -> List[str]:
"""
Refresh stored ohlcv data for backtesting and hyperopt operations.
Used by freqtrade download-data
:return: Pairs not available
"""
pairs_not_available = []
for pair in pairs:
if pair not in exchange.markets:
pairs_not_available.append(pair)
logger.info(f"Skipping pair {pair}...")
continue
for ticker_interval in timeframes:
dl_file = pair_data_filename(dl_path, pair, ticker_interval)
if erase and dl_file.exists():
logger.info(
f'Deleting existing data for pair {pair}, interval {ticker_interval}.')
dl_file.unlink()
logger.info(f'Downloading pair {pair}, interval {ticker_interval}.')
download_pair_history(datadir=dl_path, exchange=exchange,
pair=pair, ticker_interval=str(ticker_interval),
timerange=timerange)
return pairs_not_available
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
""" """
Get the maximum timeframe for the given backtest data Get the maximum timeframe for the given backtest data

View File

@ -2,6 +2,9 @@
import logging import logging
from typing import Dict from typing import Dict
import ccxt
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,3 +28,53 @@ class Binance(Exchange):
limit = min(list(filter(lambda x: limit <= x, limit_range))) limit = min(list(filter(lambda x: limit <= x, limit_range)))
return super().get_order_book(pair, limit) return super().get_order_book(pair, limit)
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
"""
creates a stoploss limit order.
this stoploss-limit is binance-specific.
It may work with a limited number of other exchanges, but this has not been tested yet.
"""
ordertype = "stop_loss_limit"
stop_price = self.symbol_price_prec(pair, stop_price)
# Ensure rate is less than stop price
if stop_price <= rate:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
if self._config['dry_run']:
dry_order = self.dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order
try:
params = self._params.copy()
params.update({'stopPrice': stop_price})
amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate)
order = self._api.create_order(pair, ordertype, 'sell',
amount, rate, params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
return order
except ccxt.InsufficientFunds as e:
raise DependencyException(
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate}.'
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}.'
f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e

View File

@ -14,6 +14,7 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
import ccxt import ccxt
import ccxt.async_support as ccxt_async import ccxt.async_support as ccxt_async
from ccxt.base.decimal_to_precision import ROUND_UP, ROUND_DOWN
from pandas import DataFrame from pandas import DataFrame
from freqtrade import (DependencyException, InvalidOrderException, from freqtrade import (DependencyException, InvalidOrderException,
@ -320,7 +321,7 @@ class Exchange(object):
if (order_types.get("stoploss_on_exchange") if (order_types.get("stoploss_on_exchange")
and not self._ft_has.get("stoploss_on_exchange", False)): and not self._ft_has.get("stoploss_on_exchange", False)):
raise OperationalException( raise OperationalException(
'On exchange stoploss is not supported for %s.' % self.name f'On exchange stoploss is not supported for {self.name}.'
) )
def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
@ -450,30 +451,14 @@ class Exchange(object):
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
""" """
creates a stoploss limit order. creates a stoploss limit order.
NOTICE: it is not supported by all exchanges. only binance is tested for now. Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
TODO: implementation maybe needs to be moved to the binance subclass exchange's subclass.
The exception below should never raise, since we disallow
starting the bot in validate_ordertypes()
Note: Changes to this interface need to be applied to all sub-classes too.
""" """
ordertype = "stop_loss_limit"
stop_price = self.symbol_price_prec(pair, stop_price) raise OperationalException(f"stoploss_limit is not implemented for {self.name}.")
# Ensure rate is less than stop price
if stop_price <= rate:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
if self._config['dry_run']:
dry_order = self.dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order
params = self._params.copy()
params.update({'stopPrice': stop_price})
order = self.create_order(pair, ordertype, 'sell', amount, rate, params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
return order
@retrier @retrier
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:
@ -824,11 +809,9 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
""" """
if not date: if not date:
date = datetime.now(timezone.utc) date = datetime.now(timezone.utc)
timeframe_secs = timeframe_to_seconds(timeframe)
# Get offset based on timerame_secs new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
offset = date.timestamp() % timeframe_secs ROUND_DOWN) // 1000
# Subtract seconds passed since last offset
new_timestamp = date.timestamp() - offset
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
@ -839,9 +822,8 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
:param date: date to use. Defaults to utcnow() :param date: date to use. Defaults to utcnow()
:returns: date of next candle (with utc timezone) :returns: date of next candle (with utc timezone)
""" """
prevdate = timeframe_to_prev_date(timeframe, date) if not date:
timeframe_secs = timeframe_to_seconds(timeframe) date = datetime.now(timezone.utc)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
# Add one interval to previous candle ROUND_UP) // 1000
new_timestamp = prevdate.timestamp() + timeframe_secs
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)

View File

@ -216,7 +216,7 @@ class FreqtradeBot(object):
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
open_trades = len(Trade.get_open_trades()) open_trades = len(Trade.get_open_trades())
if open_trades >= self.config['max_open_trades']: if open_trades >= self.config['max_open_trades']:
logger.warning('Can\'t open a new trade: max number of trades is reached') logger.warning("Can't open a new trade: max number of trades is reached")
return None return None
return available_amount / (self.config['max_open_trades'] - open_trades) return available_amount / (self.config['max_open_trades'] - open_trades)
@ -351,8 +351,8 @@ class FreqtradeBot(object):
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested) min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested)
if min_stake_amount is not None and min_stake_amount > stake_amount: if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning( logger.warning(
f'Can\'t open a new trade for {pair_s}: stake amount ' f"Can't open a new trade for {pair_s}: stake amount "
f'is too small ({stake_amount} < {min_stake_amount})' f"is too small ({stake_amount} < {min_stake_amount})"
) )
return False return False
@ -662,6 +662,7 @@ class FreqtradeBot(object):
return False return False
except DependencyException as exception: except DependencyException as exception:
trade.stoploss_order_id = None
logger.warning('Unable to place a stoploss order on exchange: %s', exception) 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
@ -674,6 +675,7 @@ class FreqtradeBot(object):
trade.stoploss_order_id = str(stoploss_order_id) trade.stoploss_order_id = str(stoploss_order_id)
return False return False
except DependencyException as exception: except DependencyException as exception:
trade.stoploss_order_id = None
logger.warning('Stoploss order was cancelled, ' logger.warning('Stoploss order was cancelled, '
'but unable to recreate one: %s', exception) 'but unable to recreate one: %s', exception)
@ -726,7 +728,8 @@ class FreqtradeBot(object):
)['id'] )['id']
trade.stoploss_order_id = str(stoploss_order_id) trade.stoploss_order_id = str(stoploss_order_id)
except DependencyException: except DependencyException:
logger.exception(f"Could create trailing stoploss order " 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,

View File

@ -81,6 +81,12 @@ class Backtesting(object):
# No strategy list specified, only one strategy # No strategy list specified, only one strategy
self.strategylist.append(StrategyResolver(self.config).strategy) self.strategylist.append(StrategyResolver(self.config).strategy)
if "ticker_interval" not in self.config:
raise OperationalException("Ticker-interval needs to be set in either configuration "
"or as cli argument `--ticker-interval 5m`")
self.ticker_interval = str(self.config.get('ticker_interval'))
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
# Load one (first) strategy # Load one (first) strategy
self._set_strategy(self.strategylist[0]) self._set_strategy(self.strategylist[0])
@ -89,12 +95,6 @@ class Backtesting(object):
Load strategy into backtesting Load strategy into backtesting
""" """
self.strategy = strategy self.strategy = strategy
if "ticker_interval" not in self.config:
raise OperationalException("Ticker-interval needs to be set in either configuration "
"or as cli argument `--ticker-interval 5m`")
self.ticker_interval = self.config.get('ticker_interval')
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
self.advise_buy = strategy.advise_buy self.advise_buy = strategy.advise_buy
self.advise_sell = strategy.advise_sell self.advise_sell = strategy.advise_sell
# Set stoploss_on_exchange to false for backtesting, # Set stoploss_on_exchange to false for backtesting,

View File

@ -37,7 +37,7 @@ INITIAL_POINTS = 30
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
class Hyperopt(Backtesting): class Hyperopt:
""" """
Hyperopt class, this class contains all the logic to run a hyperopt simulation Hyperopt class, this class contains all the logic to run a hyperopt simulation
@ -46,7 +46,9 @@ class Hyperopt(Backtesting):
hyperopt.start() hyperopt.start()
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
super().__init__(config) self.config = config
self.backtesting = Backtesting(self.config)
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
@ -70,10 +72,10 @@ class Hyperopt(Backtesting):
# Populate functions here (hasattr is slow so should not be run during "regular" operations) # Populate functions here (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_buy_trend'): if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore self.backtesting.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
if hasattr(self.custom_hyperopt, 'populate_sell_trend'): if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore self.backtesting.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True): if self.config.get('use_max_market_positions', True):
@ -122,14 +124,14 @@ class Hyperopt(Backtesting):
Save hyperopt trials to file Save hyperopt trials to file
""" """
if self.trials: if self.trials:
logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file) logger.info("Saving %d evaluations to '%s'", len(self.trials), self.trials_file)
dump(self.trials, self.trials_file) dump(self.trials, self.trials_file)
def read_trials(self) -> List: def read_trials(self) -> List:
""" """
Read hyperopt trials file Read hyperopt trials file
""" """
logger.info('Reading Trials from \'%s\'', self.trials_file) logger.info("Reading Trials from '%s'", self.trials_file)
trials = load(self.trials_file) trials = load(self.trials_file)
self.trials_file.unlink() self.trials_file.unlink()
return trials return trials
@ -249,22 +251,22 @@ class Hyperopt(Backtesting):
""" """
params = self.get_args(_params) params = self.get_args(_params)
if self.has_space('roi'): if self.has_space('roi'):
self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params)
if self.has_space('buy'): if self.has_space('buy'):
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) self.backtesting.advise_buy = self.custom_hyperopt.buy_strategy_generator(params)
if self.has_space('sell'): if self.has_space('sell'):
self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) self.backtesting.advise_sell = self.custom_hyperopt.sell_strategy_generator(params)
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.strategy.stoploss = params['stoploss'] self.backtesting.strategy.stoploss = params['stoploss']
processed = load(self.tickerdata_pickle) processed = load(self.tickerdata_pickle)
min_date, max_date = get_timeframe(processed) min_date, max_date = get_timeframe(processed)
results = self.backtest( results = self.backtesting.backtest(
{ {
'stake_amount': self.config['stake_amount'], 'stake_amount': self.config['stake_amount'],
'processed': processed, 'processed': processed,
@ -345,9 +347,9 @@ class Hyperopt(Backtesting):
data = load_data( data = load_data(
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
pairs=self.config['exchange']['pair_whitelist'], pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.ticker_interval, ticker_interval=self.backtesting.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False), refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.exchange, exchange=self.backtesting.exchange,
timerange=timerange timerange=timerange
) )
@ -364,20 +366,20 @@ class Hyperopt(Backtesting):
(max_date - min_date).days (max_date - min_date).days
) )
self.strategy.advise_indicators = \ self.backtesting.strategy.advise_indicators = \
self.custom_hyperopt.populate_indicators # type: ignore self.custom_hyperopt.populate_indicators # type: ignore
preprocessed = self.strategy.tickerdata_to_dataframe(data) preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
dump(preprocessed, self.tickerdata_pickle) dump(preprocessed, self.tickerdata_pickle)
# We don't need exchange instance anymore while running hyperopt # We don't need exchange instance anymore while running hyperopt
self.exchange = None # type: ignore self.backtesting.exchange = None # type: ignore
self.load_previous_results() self.load_previous_results()
cpus = cpu_count() cpus = cpu_count()
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!') logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
config_jobs = self.config.get('hyperopt_jobs', -1) config_jobs = self.config.get('hyperopt_jobs', -1)
logger.info(f'Number of parallel jobs set as: {config_jobs}') logger.info(f'Number of parallel jobs set as: {config_jobs}')

View File

@ -48,8 +48,8 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
try: try:
engine = create_engine(db_url, **kwargs) engine = create_engine(db_url, **kwargs)
except NoSuchModuleError: except NoSuchModuleError:
raise OperationalException(f'Given value for db_url: \'{db_url}\' ' raise OperationalException(f"Given value for db_url: '{db_url}' "
f'is no valid database URL! (See {_SQL_DOCS_URL})') f"is no valid database URL! (See {_SQL_DOCS_URL})")
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session() Trade.session = session()

View File

@ -153,6 +153,10 @@ class StrategyResolver(IResolver):
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
if any([x == 2 for x in [strategy._populate_fun_len,
strategy._buy_fun_len,
strategy._sell_fun_len]]):
strategy.INTERFACE_VERSION = 1
try: try:
return import_strategy(strategy, config=config) return import_strategy(strategy, config=config)

View File

@ -56,7 +56,10 @@ class RPCManager(object):
logger.info('Sending rpc message: %s', msg) logger.info('Sending rpc message: %s', msg)
for mod in self.registered_modules: for mod in self.registered_modules:
logger.debug('Forwarding message to rpc.%s', mod.name) logger.debug('Forwarding message to rpc.%s', mod.name)
mod.send_msg(msg) try:
mod.send_msg(msg)
except NotImplementedError:
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
def startup_messages(self, config, pairlist) -> None: def startup_messages(self, config, pairlist) -> None:
if config.get('dry_run', False): if config.get('dry_run', False):

View File

@ -43,7 +43,9 @@ class Webhook(RPC):
valuedict = self._config['webhook'].get('webhookbuy', None) valuedict = self._config['webhook'].get('webhookbuy', None)
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhooksell', None) valuedict = self._config['webhook'].get('webhooksell', None)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.CUSTOM_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION):
valuedict = self._config['webhook'].get('webhookstatus', None) valuedict = self._config['webhook'].get('webhookstatus', None)
else: else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) raise NotImplementedError('Unknown message type: {}'.format(msg['type']))

View File

@ -13,6 +13,7 @@ class DefaultStrategy(IStrategy):
Default Strategy provided by freqtrade bot. Default Strategy provided by freqtrade bot.
You can override it with your own strategy You can override it with your own strategy
""" """
INTERFACE_VERSION = 2
# Minimal ROI designed for the strategy # Minimal ROI designed for the strategy
minimal_roi = { minimal_roi = {

View File

@ -60,6 +60,11 @@ class IStrategy(ABC):
stoploss -> float: optimal stoploss designed for the strategy stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> str: value of the ticker interval to use for the strategy ticker_interval -> str: value of the ticker interval to use for the strategy
""" """
# Strategy interface version
# Default to version 2
# Version 1 is the initial interface without metadata dict
# Version 2 populate_* include metadata dict
INTERFACE_VERSION: int = 2
_populate_fun_len: int = 0 _populate_fun_len: int = 0
_buy_fun_len: int = 0 _buy_fun_len: int = 0

View File

@ -5,7 +5,7 @@ import os
import uuid import uuid
from pathlib import Path from pathlib import Path
from shutil import copyfile from shutil import copyfile
from unittest.mock import MagicMock from unittest.mock import MagicMock, PropertyMock
import arrow import arrow
import pytest import pytest
@ -17,6 +17,7 @@ from freqtrade.data import history
from freqtrade.data.history import (download_pair_history, from freqtrade.data.history import (download_pair_history,
load_cached_data_for_updating, load_cached_data_for_updating,
load_tickerdata_file, make_testdata_path, load_tickerdata_file, make_testdata_path,
refresh_backtest_ohlcv_data,
trim_tickerlist) trim_tickerlist)
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import file_dump_json from freqtrade.misc import file_dump_json
@ -558,3 +559,43 @@ def test_validate_backtest_data(default_conf, mocker, caplog) -> None:
assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC',
min_date, max_date, timeframe_to_minutes('5m')) min_date, max_date, timeframe_to_minutes('5m'))
assert len(caplog.record_tuples) == 0 assert len(caplog.record_tuples) == 0
def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog):
dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock())
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch.object(Path, "unlink", MagicMock())
ex = get_patched_exchange(mocker, default_conf)
timerange = TimeRange.parse_timerange("20190101-20190102")
refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"],
timeframes=["1m", "5m"], dl_path=make_testdata_path(None),
timerange=timerange, erase=True
)
assert dl_mock.call_count == 4
assert dl_mock.call_args[1]['timerange'].starttype == 'date'
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
def test_download_data_no_markets(mocker, default_conf, caplog):
dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock())
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
)
ex = get_patched_exchange(mocker, default_conf)
timerange = TimeRange.parse_timerange("20190101-20190102")
unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"],
timeframes=["1m", "5m"],
dl_path=make_testdata_path(None),
timerange=timerange, erase=False
)
assert dl_mock.call_count == 0
assert "ETH/BTC" in unav_pairs
assert "XRP/BTC" in unav_pairs
assert log_has("Skipping pair ETH/BTC...", caplog)

View File

@ -0,0 +1,90 @@
from random import randint
from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.tests.conftest import get_patched_exchange
def test_stoploss_limit_order(default_conf, mocker):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop_loss_limit'
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'sell'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] == 200
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
def test_stoploss_limit_order_dry_run(default_conf, mocker):
api_mock = MagicMock()
order_type = 'stop_loss_limit'
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == order_type
assert order['price'] == 220
assert order['amount'] == 1

View File

@ -101,18 +101,21 @@ def test_destroy(default_conf, mocker, caplog):
def test_init_exception(default_conf, mocker): def test_init_exception(default_conf, mocker):
default_conf['exchange']['name'] = 'wrong_exchange_name' default_conf['exchange']['name'] = 'wrong_exchange_name'
with pytest.raises( with pytest.raises(OperationalException,
OperationalException, match=f"Exchange {default_conf['exchange']['name']} is not supported"):
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
Exchange(default_conf) Exchange(default_conf)
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
with pytest.raises( with pytest.raises(OperationalException,
OperationalException, match=f"Exchange {default_conf['exchange']['name']} is not supported"):
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError)) mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError))
Exchange(default_conf) Exchange(default_conf)
with pytest.raises(OperationalException,
match=r"Initialization of ccxt failed. Reason: DeadBeef"):
mocker.patch("ccxt.binance", MagicMock(side_effect=ccxt.BaseError("DeadBeef")))
Exchange(default_conf)
def test_exchange_resolver(default_conf, mocker, caplog): def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock()))
@ -1436,87 +1439,11 @@ def test_get_fee(default_conf, mocker, exchange_name):
'get_fee', 'calculate_fee') 'get_fee', 'calculate_fee')
def test_stoploss_limit_order(default_conf, mocker): def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker):
api_mock = MagicMock() exchange = get_patched_exchange(mocker, default_conf, 'bittrex')
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"):
order_type = 'stop_loss_limit'
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'sell'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] == 200
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
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):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(OperationalException):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
def test_stoploss_limit_order_dry_run(default_conf, mocker):
api_mock = MagicMock()
order_type = 'stop_loss_limit'
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == order_type
assert order['price'] == 220
assert order['amount'] == 1
def test_merge_ft_has_dict(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker):
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',
@ -1604,7 +1531,7 @@ def test_timeframe_to_prev_date():
assert timeframe_to_prev_date(interval, date) == result assert timeframe_to_prev_date(interval, date) == result
date = datetime.now(tz=timezone.utc) date = datetime.now(tz=timezone.utc)
assert timeframe_to_prev_date("5m", date) < date assert timeframe_to_prev_date("5m") < date
def test_timeframe_to_next_date(): def test_timeframe_to_next_date():
@ -1629,4 +1556,4 @@ def test_timeframe_to_next_date():
assert timeframe_to_next_date(interval, date) == result assert timeframe_to_next_date(interval, date) == result
date = datetime.now(tz=timezone.utc) date = datetime.now(tz=timezone.utc)
assert timeframe_to_next_date("5m", date) > date assert timeframe_to_next_date("5m") > date

View File

@ -330,7 +330,7 @@ def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> No
patch_exchange(mocker) patch_exchange(mocker)
del default_conf['ticker_interval'] del default_conf['ticker_interval']
default_conf['strategy_list'] = ['DefaultStrategy', default_conf['strategy_list'] = ['DefaultStrategy',
'TestStrategy'] 'SampleStrategy']
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
@ -877,7 +877,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
'--disable-max-market-positions', '--disable-max-market-positions',
'--strategy-list', '--strategy-list',
'DefaultStrategy', 'DefaultStrategy',
'TestStrategy', 'SampleStrategy',
] ]
args = get_args(args) args = get_args(args)
start_backtesting(args) start_backtesting(args)
@ -898,7 +898,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Parameter --enable-position-stacking detected ...', 'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy DefaultStrategy',
'Running backtesting for Strategy TestStrategy', 'Running backtesting for Strategy SampleStrategy',
] ]
for line in exists: for line in exists:

View File

@ -254,7 +254,7 @@ def test_start_failure(mocker, default_conf, caplog) -> None:
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'TestStrategy', '--strategy', 'SampleStrategy',
'hyperopt', 'hyperopt',
'--epochs', '5' '--epochs', '5'
] ]
@ -381,7 +381,7 @@ def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
hyperopt.save_trials() hyperopt.save_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has('Saving 1 evaluations to \'{}\''.format(trials_file), caplog) assert log_has("Saving 1 evaluations to '{}'".format(trials_file), caplog)
mock_dump.assert_called_once() mock_dump.assert_called_once()
@ -390,7 +390,7 @@ def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials) mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
hyperopt_trial = hyperopt.read_trials() hyperopt_trial = hyperopt.read_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has('Reading Trials from \'{}\''.format(trials_file), caplog) assert log_has("Reading Trials from '{}'".format(trials_file), caplog)
assert hyperopt_trial == trials assert hyperopt_trial == trials
mock_load.assert_called_once() mock_load.assert_called_once()
@ -429,7 +429,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
'hyperopt_jobs': 1, }) 'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -441,8 +441,8 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for tickerdata, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
assert hasattr(hyperopt, "advise_sell") assert hasattr(hyperopt.backtesting, "advise_sell")
assert hasattr(hyperopt, "advise_buy") assert hasattr(hyperopt.backtesting, "advise_buy")
assert hasattr(hyperopt, "max_open_trades") assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == default_conf['max_open_trades'] assert hyperopt.max_open_trades == default_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")
@ -488,7 +488,7 @@ def test_populate_indicators(hyperopt) -> None:
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
fill_missing=True)} fill_missing=True)}
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -502,7 +502,7 @@ def test_buy_strategy_generator(hyperopt) -> None:
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
fill_missing=True)} fill_missing=True)}
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -538,7 +538,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
backtest_result = pd.DataFrame.from_records(trades, columns=labels) backtest_result = pd.DataFrame.from_records(trades, columns=labels)
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.backtest', 'freqtrade.optimize.hyperopt.Backtesting.backtest',
MagicMock(return_value=backtest_result) MagicMock(return_value=backtest_result)
) )
mocker.patch( mocker.patch(
@ -644,7 +644,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
}) })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -681,7 +681,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
}) })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()

View File

@ -115,6 +115,22 @@ def test_init_webhook_enabled(mocker, default_conf, caplog) -> None:
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG)
default_conf['telegram']['enabled'] = False
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
mocker.patch('freqtrade.rpc.webhook.Webhook.send_msg',
MagicMock(side_effect=NotImplementedError))
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
rpc_manager.send_msg({'type': RPCMessageType.CUSTOM_NOTIFICATION,
'status': 'TestMessage'})
assert log_has(
"Message type RPCMessageType.CUSTOM_NOTIFICATION not implemented by handler webhook.",
caplog)
def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None:
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())

View File

@ -91,21 +91,24 @@ def test_send_msg(default_conf, mocker):
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksell"]["value3"].format(**msg)) default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
# Test notification for msgtype in [RPCMessageType.STATUS_NOTIFICATION,
msg = { RPCMessageType.WARNING_NOTIFICATION,
'type': RPCMessageType.STATUS_NOTIFICATION, RPCMessageType.CUSTOM_NOTIFICATION]:
'status': 'Unfilled sell order for BTC cancelled due to timeout' # Test notification
} msg = {
msg_mock = MagicMock() 'type': msgtype,
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) 'status': 'Unfilled sell order for BTC cancelled due to timeout'
webhook.send_msg(msg) }
assert msg_mock.call_count == 1 msg_mock = MagicMock()
assert (msg_mock.call_args[0][0]["value1"] == mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
default_conf["webhook"]["webhookstatus"]["value1"].format(**msg)) webhook.send_msg(msg)
assert (msg_mock.call_args[0][0]["value2"] == assert msg_mock.call_count == 1
default_conf["webhook"]["webhookstatus"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value1"] ==
assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookstatus"]["value1"].format(**msg))
default_conf["webhook"]["webhookstatus"]["value3"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookstatus"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookstatus"]["value3"].format(**msg))
def test_exception_send_msg(default_conf, mocker, caplog): def test_exception_send_msg(default_conf, mocker, caplog):

View File

@ -15,7 +15,7 @@ class TestStrategyLegacy(IStrategy):
""" """
This is a test strategy using the legacy function headers, which will be This is a test strategy using the legacy function headers, which will be
removed in a future update. removed in a future update.
Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py Please do not use this as a template, but refer to user_data/strategy/sample_strategy.py
for a uptodate version of this template. for a uptodate version of this template.
""" """

View File

@ -61,27 +61,27 @@ def test_search_strategy():
def test_load_strategy(default_conf, result): def test_load_strategy(default_conf, result):
default_conf.update({'strategy': 'TestStrategy'}) default_conf.update({'strategy': 'SampleStrategy'})
resolver = StrategyResolver(default_conf) resolver = StrategyResolver(default_conf)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_base64(result, caplog, default_conf):
with open("user_data/strategies/test_strategy.py", "rb") as file: with open("user_data/strategies/sample_strategy.py", "rb") as file:
encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") encoded_string = urlsafe_b64encode(file.read()).decode("utf-8")
default_conf.update({'strategy': 'TestStrategy:{}'.format(encoded_string)}) default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)})
resolver = StrategyResolver(default_conf) resolver = StrategyResolver(default_conf)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
# Make sure strategy was loaded from base64 (using temp directory)!! # Make sure strategy was loaded from base64 (using temp directory)!!
assert log_has_re(r"Using resolved strategy TestStrategy from '" assert log_has_re(r"Using resolved strategy SampleStrategy from '"
+ tempfile.gettempdir() + r"/.*/TestStrategy\.py'\.\.\.", caplog) + tempfile.gettempdir() + r"/.*/SampleStrategy\.py'\.\.\.", caplog)
def test_load_strategy_invalid_directory(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf):
resolver = StrategyResolver(default_conf) resolver = StrategyResolver(default_conf)
extra_dir = Path.cwd() / 'some/path' extra_dir = Path.cwd() / 'some/path'
resolver._load_strategy('TestStrategy', config=default_conf, extra_dir=extra_dir) resolver._load_strategy('SampleStrategy', config=default_conf, extra_dir=extra_dir)
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog)
@ -380,6 +380,31 @@ def test_call_deprecated_function(result, monkeypatch, default_conf):
assert resolver.strategy._populate_fun_len == 2 assert resolver.strategy._populate_fun_len == 2
assert resolver.strategy._buy_fun_len == 2 assert resolver.strategy._buy_fun_len == 2
assert resolver.strategy._sell_fun_len == 2 assert resolver.strategy._sell_fun_len == 2
assert resolver.strategy.INTERFACE_VERSION == 1
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
assert isinstance(indicator_df, DataFrame)
assert 'adx' in indicator_df.columns
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
assert isinstance(buydf, DataFrame)
assert 'buy' in buydf.columns
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
assert isinstance(selldf, DataFrame)
assert 'sell' in selldf
def test_strategy_interface_versioning(result, monkeypatch, default_conf):
default_conf.update({'strategy': 'DefaultStrategy'})
resolver = StrategyResolver(default_conf)
metadata = {'pair': 'ETH/BTC'}
# Make sure we are using a legacy function
assert resolver.strategy._populate_fun_len == 3
assert resolver.strategy._buy_fun_len == 3
assert resolver.strategy._sell_fun_len == 3
assert resolver.strategy.INTERFACE_VERSION == 2
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
assert isinstance(indicator_df, DataFrame) assert isinstance(indicator_df, DataFrame)

View File

@ -101,7 +101,7 @@ def test_parse_args_backtesting_custom() -> None:
'--refresh-pairs-cached', '--refresh-pairs-cached',
'--strategy-list', '--strategy-list',
'DefaultStrategy', 'DefaultStrategy',
'TestStrategy' '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']

View File

@ -41,14 +41,14 @@ def test_load_config_invalid_pair(default_conf) -> None:
def test_load_config_missing_attributes(default_conf) -> None: def test_load_config_missing_attributes(default_conf) -> None:
default_conf.pop('exchange') default_conf.pop('exchange')
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'): with pytest.raises(ValidationError, match=r".*'exchange' is a required property.*"):
validate_config_schema(default_conf) validate_config_schema(default_conf)
def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_incorrect_stake_amount(default_conf) -> None:
default_conf['stake_amount'] = 'fake' default_conf['stake_amount'] = 'fake'
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'): with pytest.raises(ValidationError, match=r".*'fake' does not match 'unlimited'.*"):
validate_config_schema(default_conf) validate_config_schema(default_conf)
@ -472,7 +472,7 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
assert 'spaces' in config assert 'spaces' in config
assert config['spaces'] == ['all'] assert config['spaces'] == ['all']
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog) assert log_has("Parameter -s/--spaces detected: ['all']", caplog)
assert "runmode" in config assert "runmode" in config
assert config['runmode'] == RunMode.HYPEROPT assert config['runmode'] == RunMode.HYPEROPT
@ -722,7 +722,7 @@ def test_load_config_default_exchange(all_conf) -> None:
assert 'exchange' not in all_conf assert 'exchange' not in all_conf
with pytest.raises(ValidationError, with pytest.raises(ValidationError,
match=r'\'exchange\' is a required property'): match=r"'exchange' is a required property"):
validate_config_schema(all_conf) validate_config_schema(all_conf)
@ -736,7 +736,7 @@ def test_load_config_default_exchange_name(all_conf) -> None:
assert 'name' not in all_conf['exchange'] assert 'name' not in all_conf['exchange']
with pytest.raises(ValidationError, with pytest.raises(ValidationError,
match=r'\'name\' is a required property'): match=r"'name' is a required property"):
validate_config_schema(all_conf) validate_config_schema(all_conf)
@ -871,3 +871,4 @@ def test_pairlist_resolving_fallback(mocker):
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
assert config['exchange']['name'] == 'binance' assert config['exchange']['name'] == 'binance'
assert config['datadir'] == str(Path.cwd() / "user_data/data/binance")

View File

@ -1112,6 +1112,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
# Third case: when stoploss was set but it was canceled for some reason # Third case: when stoploss was set but it was canceled for some reason
# should set a stoploss immediately and return False # should set a stoploss immediately and return False
caplog.clear()
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
trade.stoploss_order_id = 100 trade.stoploss_order_id = 100
@ -1127,6 +1128,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
# Fourth case: when stoploss is set and it is hit # Fourth case: when stoploss is set and it is hit
# should unset stoploss_order_id and return true # should unset stoploss_order_id and return true
# as a trade actually happened # as a trade actually happened
caplog.clear()
freqtrade.create_trades() freqtrade.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
trade.is_open = True trade.is_open = True
@ -1152,6 +1154,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
) )
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
# Fifth case: get_order returns InvalidOrder # Fifth case: get_order returns InvalidOrder
# It should try to add stoploss order # It should try to add stoploss order
@ -1163,6 +1166,41 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
assert stoploss_limit.call_count == 1 assert stoploss_limit.call_count == 1
def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
markets, limit_buy_order, limit_sell_order) -> None:
# Sixth case: stoploss order was cancelled but couldn't create new one
patch_RPCManager(mocker)
patch_exchange(mocker)
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=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
markets=PropertyMock(return_value=markets),
get_order=MagicMock(return_value={'status': 'canceled'}),
stoploss_limit=MagicMock(side_effect=DependencyException()),
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.create_trades()
trade = Trade.query.first()
trade.is_open = True
trade.open_order_id = '12345'
trade.stoploss_order_id = 100
assert trade
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert log_has_re(r'Stoploss order was cancelled, but unable to recreate one.*', caplog)
assert trade.stoploss_order_id is None
assert trade.is_open is True
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
@ -1324,7 +1362,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException()) mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException())
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 1 assert cancel_mock.call_count == 1
assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*", caplog) assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
@ -2376,7 +2414,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf,
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@ -2416,7 +2454,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf,
freqtrade.process_maybe_execute_sell(trade) freqtrade.process_maybe_execute_sell(trade)
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
assert trade.is_open is False assert trade.is_open is False
print(trade.sell_reason)
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2

View File

@ -1,5 +1,4 @@
import re import re
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import pytest import pytest
@ -70,74 +69,8 @@ def test_create_datadir(caplog, mocker):
assert len(caplog.record_tuples) == 0 assert len(caplog.record_tuples) == 0
def test_download_data(mocker, markets, caplog):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch.object(Path, "unlink", MagicMock())
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
"--erase",
]
start_download_data(get_args(args))
assert dl_mock.call_count == 4
assert dl_mock.call_args[1]['timerange'].starttype is None
assert dl_mock.call_args[1]['timerange'].stoptype is None
assert log_has("Deleting existing data for pair ETH/BTC, interval 1m.", caplog)
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
def test_download_data_days(mocker, markets, caplog):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch.object(Path, "unlink", MagicMock())
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
"--days", "20",
]
start_download_data(get_args(args))
assert dl_mock.call_count == 4
assert dl_mock.call_args[1]['timerange'].starttype == 'date'
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
def test_download_data_no_markets(mocker, caplog):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
)
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
]
start_download_data(get_args(args))
assert dl_mock.call_count == 0
assert log_has("Skipping pair ETH/BTC...", caplog)
assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog)
def test_download_data_keyboardInterrupt(mocker, caplog, markets): def test_download_data_keyboardInterrupt(mocker, caplog, markets):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data',
MagicMock(side_effect=KeyboardInterrupt)) MagicMock(side_effect=KeyboardInterrupt))
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch( mocker.patch(
@ -152,3 +85,21 @@ def test_download_data_keyboardInterrupt(mocker, caplog, markets):
start_download_data(get_args(args)) start_download_data(get_args(args))
assert dl_mock.call_count == 1 assert dl_mock.call_count == 1
def test_download_data_no_markets(mocker, caplog):
dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data',
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
)
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
"--days", "20"
]
start_download_data(get_args(args))
assert dl_mock.call_args[1]['timerange'].starttype == "date"
assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog)

View File

@ -2,13 +2,13 @@ import logging
import sys import sys
from argparse import Namespace from argparse import Namespace
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict, List
import arrow import arrow
from freqtrade.configuration import Configuration, TimeRange from freqtrade.configuration import Configuration, TimeRange
from freqtrade.configuration.directory_operations import create_userdata_dir from freqtrade.configuration.directory_operations import create_userdata_dir
from freqtrade.data.history import download_pair_history from freqtrade.data.history import refresh_backtest_ohlcv_data
from freqtrade.exchange import available_exchanges from freqtrade.exchange import available_exchanges
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -75,36 +75,20 @@ def start_download_data(args: Namespace) -> None:
logger.info(f'About to download pairs: {config["pairs"]}, ' logger.info(f'About to download pairs: {config["pairs"]}, '
f'intervals: {config["timeframes"]} to {dl_path}') f'intervals: {config["timeframes"]} to {dl_path}')
pairs_not_available = [] pairs_not_available: List[str] = []
try: try:
# Init exchange # Init exchange
exchange = ExchangeResolver(config['exchange']['name'], config).exchange exchange = ExchangeResolver(config['exchange']['name'], config).exchange
for pair in config["pairs"]: pairs_not_available = refresh_backtest_ohlcv_data(
if pair not in exchange.markets: exchange, pairs=config["pairs"], timeframes=config["timeframes"],
pairs_not_available.append(pair) dl_path=Path(config['datadir']), timerange=timerange, erase=config.get("erase"))
logger.info(f"Skipping pair {pair}...")
continue
for ticker_interval in config["timeframes"]:
pair_print = pair.replace('/', '_')
filename = f'{pair_print}-{ticker_interval}.json'
dl_file = dl_path.joinpath(filename)
if config.get("erase") and dl_file.exists():
logger.info(
f'Deleting existing data for pair {pair}, interval {ticker_interval}.')
dl_file.unlink()
logger.info(f'Downloading pair {pair}, interval {ticker_interval}.')
download_pair_history(datadir=dl_path, exchange=exchange,
pair=pair, ticker_interval=str(ticker_interval),
timerange=timerange)
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit("SIGINT received, aborting ...") sys.exit("SIGINT received, aborting ...")
finally: finally:
if pairs_not_available: if pairs_not_available:
logger.info( logger.info(f"Pairs [{','.join(pairs_not_available)}] not available "
f"Pairs [{','.join(pairs_not_available)}] not available " f"on exchange {config['exchange']['name']}.")
f"on exchange {config['exchange']['name']}.")

View File

@ -1,6 +1,6 @@
# requirements without requirements installable via conda # requirements without requirements installable via conda
# mainly used for Raspberry pi installs # mainly used for Raspberry pi installs
ccxt==1.18.1068 ccxt==1.18.1085
SQLAlchemy==1.3.7 SQLAlchemy==1.3.7
python-telegram-bot==11.1.0 python-telegram-bot==11.1.0
arrow==0.14.5 arrow==0.14.5

View File

@ -7,7 +7,7 @@ flake8==3.7.8
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==2.0.0 flake8-tidy-imports==2.0.0
mypy==0.720 mypy==0.720
pytest==5.1.0 pytest==5.1.1
pytest-asyncio==0.10.0 pytest-asyncio==0.10.0
pytest-cov==2.7.1 pytest-cov==2.7.1
pytest-mock==1.10.4 pytest-mock==1.10.4

View File

@ -2,5 +2,5 @@
-r requirements-common.txt -r requirements-common.txt
numpy==1.17.0 numpy==1.17.0
pandas==0.25.0 pandas==0.25.1
scipy==1.3.1 scipy==1.3.1

View File

@ -45,7 +45,7 @@ setup(name='freqtrade',
tests_require=['pytest', 'pytest-mock', 'pytest-cov'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[ install_requires=[
# from requirements-common.txt # from requirements-common.txt
'ccxt>=1.18', 'ccxt>=1.18.1080',
'SQLAlchemy', 'SQLAlchemy',
'python-telegram-bot', 'python-telegram-bot',
'arrow', 'arrow',
@ -76,7 +76,7 @@ setup(name='freqtrade',
'plot': plot, 'plot': plot,
'all': all_extra, 'all': all_extra,
'jupyter': jupyter, 'jupyter': jupyter,
}, },
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,

View File

@ -52,7 +52,7 @@
"# Define some constants\n", "# Define some constants\n",
"ticker_interval = \"5m\"\n", "ticker_interval = \"5m\"\n",
"# Name of the strategy class\n", "# Name of the strategy class\n",
"strategy_name = 'TestStrategy'\n", "strategy_name = 'SampleStrategy'\n",
"# Path to user data\n", "# Path to user data\n",
"user_data_dir = 'user_data'\n", "user_data_dir = 'user_data'\n",
"# Location of the strategy\n", "# Location of the strategy\n",

View File

@ -11,10 +11,9 @@ import numpy # noqa
# This class is a sample. Feel free to customize it. # This class is a sample. Feel free to customize it.
class TestStrategy(IStrategy): class SampleStrategy(IStrategy):
__test__ = False # pytest expects to find tests here because of the name
""" """
This is a test strategy to inspire you. This is a sample strategy to inspire you.
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
You can: You can:
@ -28,6 +27,9 @@ class TestStrategy(IStrategy):
- the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend,
populate_sell_trend, hyperopt_space, buy_strategy_generator populate_sell_trend, hyperopt_space, buy_strategy_generator
""" """
# Strategy intervace version - allow new iterations of the strategy interface.
# Check the documentation or the Sample strategy to get the latest version.
INTERFACE_VERSION = 2
# Minimal ROI designed for the strategy. # Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi" # This attribute will be overridden if the config file contains "minimal_roi"
@ -256,14 +258,14 @@ class TestStrategy(IStrategy):
# Retrieve best bid and best ask # Retrieve best bid and best ask
# ------------------------------------ # ------------------------------------
""" """
# first check if dataprovider is available # first check if dataprovider is available
if self.dp: if self.dp:
if self.dp.runmode in ('live', 'dry_run'): if self.dp.runmode in ('live', 'dry_run'):
ob = self.dp.orderbook(metadata['pair'], 1) ob = self.dp.orderbook(metadata['pair'], 1)
dataframe['best_bid'] = ob['bids'][0][0] dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0] dataframe['best_ask'] = ob['asks'][0][0]
""" """
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: