Merge branch 'develop' into ccxt-async

This commit is contained in:
misagh 2018-07-31 12:48:12 +02:00
commit 154e4569d7
33 changed files with 942 additions and 677 deletions

View File

@ -15,7 +15,8 @@ WORKDIR /freqtrade
# Install dependencies # Install dependencies
COPY requirements.txt /freqtrade/ COPY requirements.txt /freqtrade/
RUN pip install -r requirements.txt RUN pip install numpy \
&& pip install -r requirements.txt
# Install and execute # Install and execute
COPY . /freqtrade/ COPY . /freqtrade/

View File

@ -50,6 +50,7 @@ hesitate to read the source code and understand the mechanism of this bot.
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) - [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
- [Basic Usage](#basic-usage) - [Basic Usage](#basic-usage)
- [Bot commands](#bot-commands) - [Bot commands](#bot-commands)
- [Telegram RPC commands](#telegram-rpc-commands) - [Telegram RPC commands](#telegram-rpc-commands)
@ -61,6 +62,7 @@ hesitate to read the source code and understand the mechanism of this bot.
- [Requirements](#requirements) - [Requirements](#requirements)
- [Min hardware required](#min-hardware-required) - [Min hardware required](#min-hardware-required)
- [Software requirements](#software-requirements) - [Software requirements](#software-requirements)
## Quick start ## Quick start

View File

@ -39,7 +39,6 @@ A strategy file contains all the information needed to build a good strategy:
- Sell strategy rules - Sell strategy rules
- Minimal ROI recommended - Minimal ROI recommended
- Stoploss recommended - Stoploss recommended
- Hyperopt parameter
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 `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
You can test it with the parameter: `--strategy TestStrategy` You can test it with the parameter: `--strategy TestStrategy`
@ -61,22 +60,22 @@ file as reference.**
### Buy strategy ### Buy strategy
Edit the method `populate_buy_trend()` into your strategy file to Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy.
update your buy strategy.
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/test_strategy.py`:
```python ```python
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 30) & (dataframe['adx'] > 30) &
(dataframe['tema'] <= dataframe['blower']) & (dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1)) (dataframe['tema'] > dataframe['tema'].shift(1))
), ),
'buy'] = 1 'buy'] = 1
@ -87,38 +86,47 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
### Sell strategy ### Sell strategy
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy. Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration.
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/test_strategy.py`:
```python ```python
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 70) & (dataframe['adx'] > 70) &
(dataframe['tema'] > dataframe['blower']) & (dataframe['tema'] > dataframe['bb_middleband']) &
(dataframe['tema'] < dataframe['tema'].shift(1)) (dataframe['tema'] < dataframe['tema'].shift(1))
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
``` ```
## Add more Indicator ## Add more Indicators
As you have seen, buy and sell strategies need indicators. You can add As you have seen, buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
more indicators by extending the list contained in
the method `populate_indicators()` from your strategy file. You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer.
Sample: Sample:
```python ```python
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
""" """
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
@ -149,6 +157,11 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
return dataframe return dataframe
``` ```
### Metadata dict
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
### Want more indicator examples ### 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). Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).

View File

@ -33,3 +33,4 @@ Pull-request. Do not hesitate to reach us on
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) - [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md) - [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md) - [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md))

View File

@ -56,23 +56,29 @@ Reset parameter will hard reset your branch (only if you are on `master` or `dev
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
## Manual installation - Linux/MacOS ## Manual installation - Linux/MacOS
The following steps are made for Linux/MacOS environment The following steps are made for Linux/MacOS environment
**1. Clone the repo** ### 1. Clone the repo
```bash ```bash
git clone git@github.com:freqtrade/freqtrade.git git clone git@github.com:freqtrade/freqtrade.git
git checkout develop git checkout develop
cd freqtrade cd freqtrade
``` ```
**2. Create the config file**
### 2. Create the config file
Switch `"dry_run": true,` Switch `"dry_run": true,`
```bash ```bash
cp config.json.example config.json cp config.json.example config.json
vi config.json vi config.json
``` ```
**3. Build your docker image and run it**
### 3. Build your docker image and run it
```bash ```bash
docker build -t freqtrade . docker build -t freqtrade .
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade

151
docs/sandbox-testing.md Normal file
View File

@ -0,0 +1,151 @@
# Sandbox API testing
Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these.
This document is a *light overview of configuring Freqtrade and GDAX sandbox.
This can be useful to developers and trader alike as Freqtrade is quite customisable.
When testing your API connectivity, make sure to use the following URLs.
***Website**
https://public.sandbox.gdax.com
***REST API**
https://api-public.sandbox.gdax.com
---
# Configure a Sandbox account on Gdax
Aim of this document section
- An sanbox account
- create 2FA (needed to create an API)
- Add test 50BTC to account
- Create :
- - API-KEY
- - API-Secret
- - API Password
## Acccount
This link will redirect to the sandbox main page to login / create account dialogues:
https://public.sandbox.pro.coinbase.com/orders/
After registration and Email confimation you wil be redirected into your sanbox account. It is easy to verify you're in sandbox by checking the URL bar.
> https://public.sandbox.pro.coinbase.com/
## Enable 2Fa (a prerequisite to creating sandbox API Keys)
From within sand box site select your profile, top right.
>Or as a direct link: https://public.sandbox.pro.coinbase.com/profile
From the menu panel to the left of the screen select
> Security: "*View or Update*"
In the new site select "enable authenticator" as typical google Authenticator.
- open Google Authenticator on your phone
- scan barcode
- enter your generated 2fa
## Enable API Access
From within sandbox select profile>api>create api-keys
>or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api
Click on "create one" and ensure **view** and **trade** are "checked" and sumbit your 2Fa
- **Copy and paste the Passphase** into a notepade this will be needed later
- **Copy and paste the API Secret** popup into a notepad this will needed later
- **Copy and paste the API Key** into a notepad this will needed later
## Add 50 BTC test funds
To add funds, use the web interface deposit and withdraw buttons.
To begin select 'Wallets' from the top menu.
> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets
- Deposits (bottom left of screen)
- - Deposit Funds Bitcoin
- - - Coinbase BTC Wallet
- - - - Max (50 BTC)
- - - - - Deposit
*This process may be repeated for other currencies, ETH as example*
---
# Configure Freqtrade to use Gax Sandbox
The aim of this document section
- Enable sandbox URLs in Freqtrade
- Configure API
- - secret
- - key
- - passphrase
## Sandbox URLs
Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade.
These include `['test']` and `['api']`.
- `[Test]` if available will point to an Exchanges sandbox.
- `[Api]` normally used, and resolves to live API target on the exchange
To make use of sandbox / test add "sandbox": true, to your config.json
```
"exchange": {
"name": "gdax",
"sandbox": true,
"key": "5wowfxemogxeowo;heiohgmd",
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
"password": "1bkjfkhfhfu6sr",
"pair_whitelist": [
"BTC/USD"
```
Also insert your
- api-key (noted earlier)
- api-secret (noted earlier)
- password (the passphrase - noted earlier)
---
## You should now be ready to test your sandbox!
Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox.
** Typically the BTC/USD has the most activity in sandbox to test against.
## GDAX - Old Candles problem
It is my experience that GDAX sandbox candles may be 20+- minutes out of date. This can cause trades to fail as one of Freqtrades safety checks
To disable this check, edit:
>strategy/interface.py
Look for the following section:
```
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,
(arrow.utcnow() - signal_date).seconds // 60
)
return False, False
```
You could Hash out the entire check as follows:
```
# # Check if dataframe is out of date
# signal_date = arrow.get(latest['date'])
# interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
# if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
# logger.warning(
# 'Outdated history for pair %s. Last tick is %s minutes old',
# pair,
# (arrow.utcnow() - signal_date).seconds // 60
# )
# return False, False
```
Or inrease the timeout to offer a level of protection/alignment of this test to freqtrade in live.
As example, to allow an additional 30 minutes. "(interval_minutes * 2 + 5 + 30)"
```
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5 + 30))):
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,
(arrow.utcnow() - signal_date).seconds // 60
)
return False, False
```

View File

@ -125,6 +125,7 @@ CONF_SCHEMA = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'name': {'type': 'string'}, 'name': {'type': 'string'},
'sandbox': {'type': 'boolean'},
'key': {'type': 'string'}, 'key': {'type': 'string'},
'secret': {'type': 'string'}, 'secret': {'type': 'string'},
'password': {'type': 'string'}, 'password': {'type': 'string'},

View File

@ -4,6 +4,7 @@ import logging
from random import randint from random import randint
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from datetime import datetime from datetime import datetime
from math import floor, ceil
import ccxt import ccxt
import ccxt.async_support as ccxt_async import ccxt.async_support as ccxt_async
@ -100,6 +101,8 @@ class Exchange(object):
except (KeyError, AttributeError): except (KeyError, AttributeError):
raise OperationalException(f'Exchange {name} is not supported') raise OperationalException(f'Exchange {name} is not supported')
self.set_sandbox(api, exchange_config, name)
return api return api
@property @property
@ -112,6 +115,16 @@ class Exchange(object):
"""exchange ccxt id""" """exchange ccxt id"""
return self._api.id return self._api.id
def set_sandbox(self, api, exchange_config: dict, name: str):
if exchange_config.get('sandbox'):
if api.urls.get('test'):
api.urls['api'] = api.urls['test']
logger.info("Enabled Sandbox API on %s", name)
else:
logger.warning(self, "No Sandbox URL in CCXT, exiting. "
"Please check your config.json")
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
def validate_pairs(self, pairs: List[str]) -> None: def validate_pairs(self, pairs: List[str]) -> None:
""" """
Checks if all given pairs are tradable on the current exchange. Checks if all given pairs are tradable on the current exchange.
@ -155,6 +168,28 @@ class Exchange(object):
""" """
return endpoint in self._api.has and self._api.has[endpoint] return endpoint in self._api.has and self._api.has[endpoint]
def symbol_amount_prec(self, pair, amount: float):
'''
Returns the amount to buy or sell to a precision the Exchange accepts
Rounded down
'''
if self._api.markets[pair]['precision']['amount']:
symbol_prec = self._api.markets[pair]['precision']['amount']
big_amount = amount * pow(10, symbol_prec)
amount = floor(big_amount) / pow(10, symbol_prec)
return amount
def symbol_price_prec(self, pair, price: float):
'''
Returns the price buying or selling with to the precision the Exchange accepts
Rounds up
'''
if self._api.markets[pair]['precision']['price']:
symbol_prec = self._api.markets[pair]['precision']['price']
big_price = price * pow(10, symbol_prec)
price = ceil(big_price) / pow(10, symbol_prec)
return price
def buy(self, pair: str, rate: float, amount: float) -> Dict: def buy(self, pair: str, rate: float, amount: float) -> Dict:
if self._conf['dry_run']: if self._conf['dry_run']:
order_id = f'dry_run_buy_{randint(0, 10**6)}' order_id = f'dry_run_buy_{randint(0, 10**6)}'
@ -172,6 +207,10 @@ class Exchange(object):
return {'id': order_id} return {'id': order_id}
try: try:
# Set the precision for amount and price(rate) as accepted by the exchange
amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate)
return self._api.create_limit_buy_order(pair, amount, rate) return self._api.create_limit_buy_order(pair, amount, rate)
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(
@ -205,6 +244,10 @@ class Exchange(object):
return {'id': order_id} return {'id': order_id}
try: try:
# Set the precision for amount and price(rate) as accepted by the exchange
amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate)
return self._api.create_limit_sell_order(pair, amount, rate) return self._api.create_limit_sell_order(pair, amount, rate)
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(

View File

@ -57,8 +57,8 @@ class Backtesting(object):
self.strategy: IStrategy = StrategyResolver(self.config).strategy self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.ticker_interval = self.strategy.ticker_interval self.ticker_interval = self.strategy.ticker_interval
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
self.populate_buy_trend = self.strategy.populate_buy_trend self.advise_buy = self.strategy.advise_buy
self.populate_sell_trend = self.strategy.populate_sell_trend self.advise_sell = self.strategy.advise_sell
# Reset keys for backtesting # Reset keys for backtesting
self.config['exchange']['key'] = '' self.config['exchange']['key'] = ''
@ -229,8 +229,8 @@ class Backtesting(object):
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = self.populate_sell_trend( ticker_data = self.advise_sell(
self.populate_buy_trend(pair_data))[headers].copy() self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
# to avoid using data from future, we buy/sell with signal from previous candle # to avoid using data from future, we buy/sell with signal from previous candle
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)

View File

@ -75,7 +75,7 @@ class Hyperopt(Backtesting):
return arg_dict return arg_dict
@staticmethod @staticmethod
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'] dataframe['macd'] = macd['macd']
@ -228,7 +228,7 @@ class Hyperopt(Backtesting):
""" """
Define the buy strategy parameters to be used by hyperopt Define the buy strategy parameters to be used by hyperopt
""" """
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Buy strategy Hyperopt will build and use Buy strategy Hyperopt will build and use
""" """
@ -270,7 +270,7 @@ class Hyperopt(Backtesting):
self.strategy.minimal_roi = self.generate_roi_table(params) self.strategy.minimal_roi = self.generate_roi_table(params)
if self.has_space('buy'): if self.has_space('buy'):
self.populate_buy_trend = self.buy_strategy_generator(params) self.advise_buy = self.buy_strategy_generator(params)
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.strategy.stoploss = params['stoploss'] self.strategy.stoploss = params['stoploss']
@ -351,7 +351,7 @@ class Hyperopt(Backtesting):
) )
if self.has_space('buy'): if self.has_space('buy'):
self.strategy.populate_indicators = Hyperopt.populate_indicators # type: ignore self.strategy.advise_indicators = Hyperopt.populate_indicators # type: ignore
dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
self.exchange = None # type: ignore self.exchange = None # type: ignore
self.load_previous_results() self.load_previous_results()
@ -360,7 +360,7 @@ class Hyperopt(Backtesting):
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!') logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
opt = self.get_optimizer(cpus) opt = self.get_optimizer(cpus)
EVALS = max(self.total_tries//cpus, 1) EVALS = max(self.total_tries // cpus, 1)
try: try:
with Parallel(n_jobs=cpus) as parallel: with Parallel(n_jobs=cpus) as parallel:
for i in range(EVALS): for i in range(EVALS):

View File

@ -28,13 +28,16 @@ class DefaultStrategy(IStrategy):
# Optimal ticker interval for the strategy # Optimal ticker interval for the strategy
ticker_interval = '5m' ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
""" """
# Momentum Indicator # Momentum Indicator
@ -196,10 +199,11 @@ class DefaultStrategy(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
@ -217,10 +221,11 @@ class DefaultStrategy(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[

View File

@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import Dict, List, NamedTuple, Tuple from typing import Dict, List, NamedTuple, Tuple
import warnings
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
@ -57,34 +58,45 @@ class IStrategy(ABC):
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
""" """
_populate_fun_len: int = 0
_buy_fun_len: int = 0
_sell_fun_len: int = 0
# associated minimal roi
minimal_roi: Dict minimal_roi: Dict
# associated stoploss
stoploss: float stoploss: float
# associated ticker interval
ticker_interval: str ticker_interval: str
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
self.config = config self.config = config
@abstractmethod @abstractmethod
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Populate indicators that will be used in the Buy and Sell strategy Populate indicators that will be used in the Buy and Sell strategy
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """
@abstractmethod @abstractmethod
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
@abstractmethod @abstractmethod
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with sell column :return: DataFrame with sell column
""" """
@ -94,16 +106,16 @@ class IStrategy(ABC):
""" """
return self.__class__.__name__ return self.__class__.__name__
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame: def analyze_ticker(self, ticker_history: List[Dict], metadata: dict) -> DataFrame:
""" """
Parses the given ticker history and returns a populated DataFrame Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data :return DataFrame with ticker data and indicator data
""" """
dataframe = parse_ticker_dataframe(ticker_history) dataframe = parse_ticker_dataframe(ticker_history)
dataframe = self.populate_indicators(dataframe) dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.populate_buy_trend(dataframe) dataframe = self.advise_buy(dataframe, metadata)
dataframe = self.populate_sell_trend(dataframe) dataframe = self.advise_sell(dataframe, metadata)
return dataframe return dataframe
def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]: def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]:
@ -118,7 +130,7 @@ class IStrategy(ABC):
return False, False return False, False
try: try:
dataframe = self.analyze_ticker(ticker_hist) dataframe = self.analyze_ticker(ticker_hist, {'pair': pair})
except ValueError as error: except ValueError as error:
logger.warning( logger.warning(
'Unable to analyze ticker for pair %s: %s', 'Unable to analyze ticker for pair %s: %s',
@ -263,5 +275,50 @@ class IStrategy(ABC):
""" """
Creates a dataframe and populates indicators for given ticker data Creates a dataframe and populates indicators for given ticker data
""" """
return {pair: self.populate_indicators(parse_ticker_dataframe(pair_data)) return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), {'pair': pair})
for pair, pair_data in tickerdata.items()} for pair, pair_data in tickerdata.items()}
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Populate indicators that will be used in the Buy and Sell strategy
This method should not be overridden.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
if self._populate_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_indicators(dataframe) # type: ignore
else:
return self.populate_indicators(dataframe, metadata)
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
if self._buy_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_buy_trend(dataframe) # type: ignore
else:
return self.populate_buy_trend(dataframe, metadata)
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:return: DataFrame with sell column
"""
if self._sell_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_sell_trend(dataframe) # type: ignore
else:
return self.populate_sell_trend(dataframe, metadata)

View File

@ -92,6 +92,13 @@ class StrategyResolver(object):
strategy = self._search_strategy(path, strategy_name=strategy_name, config=config) strategy = self._search_strategy(path, strategy_name=strategy_name, config=config)
if strategy: if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
strategy._populate_fun_len = len(
inspect.getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(
inspect.getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(
inspect.getfullargspec(strategy.populate_sell_trend).args)
return import_strategy(strategy, config=config) return import_strategy(strategy, config=config)
except FileNotFoundError: except FileNotFoundError:
logger.warning('Path "%s" does not exist', path) logger.warning('Path "%s" does not exist', path)

View File

@ -8,10 +8,8 @@ from unittest.mock import MagicMock
import arrow import arrow
import pytest import pytest
from jsonschema import validate
from telegram import Chat, Message, Update from telegram import Chat, Message, Update
from freqtrade import constants
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
@ -127,7 +125,6 @@ def default_conf():
"db_url": "sqlite://", "db_url": "sqlite://",
"loglevel": logging.DEBUG, "loglevel": logging.DEBUG,
} }
validate(configuration, constants.CONF_SCHEMA)
return configuration return configuration

View File

@ -1,7 +1,6 @@
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement # pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
# pragma pylint: disable=protected-access # pragma pylint: disable=protected-access
import logging import logging
from copy import deepcopy
from datetime import datetime from datetime import datetime
from random import randint from random import randint
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
@ -15,8 +14,6 @@ from freqtrade.tests.conftest import get_patched_exchange, log_has
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
"""Function to test ccxt exception handling """
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
@ -52,6 +49,93 @@ def test_init_exception(default_conf, mocker):
Exchange(default_conf) Exchange(default_conf)
def test_symbol_amount_prec(default_conf, mocker):
'''
Test rounds down to 4 Decimal places
'''
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance'))
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}})
type(api_mock).markets = markets
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
exchange = Exchange(default_conf)
amount = 2.34559
pair = 'ETH/BTC'
amount = exchange.symbol_amount_prec(pair, amount)
assert amount == 2.3455
def test_symbol_price_prec(default_conf, mocker):
'''
Test rounds up to 4 decimal places
'''
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance'))
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}})
type(api_mock).markets = markets
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
exchange = Exchange(default_conf)
price = 2.34559
pair = 'ETH/BTC'
price = exchange.symbol_price_prec(pair, price)
assert price == 2.3456
def test_set_sandbox(default_conf, mocker):
"""
Test working scenario
"""
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com",
'api': 'https://api.gdax.com'})
type(api_mock).urls = url_mock
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
exchange = Exchange(default_conf)
liveurl = exchange._api.urls['api']
default_conf['exchange']['sandbox'] = True
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
assert exchange._api.urls['api'] != liveurl
def test_set_sandbox_exception(default_conf, mocker):
"""
Test Fail scenario
"""
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'})
type(api_mock).urls = url_mock
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'):
exchange = Exchange(default_conf)
default_conf['exchange']['sandbox'] = True
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
def test_validate_pairs(default_conf, mocker): def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={ api_mock.load_markets = MagicMock(return_value={
@ -80,12 +164,11 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
api_mock.load_markets = MagicMock(return_value={ api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': '' 'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
}) })
conf = deepcopy(default_conf) default_conf['stake_currency'] = 'ETH'
conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
with pytest.raises(OperationalException, match=r'not compatible'): with pytest.raises(OperationalException, match=r'not compatible'):
Exchange(conf) Exchange(default_conf)
def test_validate_pairs_exception(default_conf, mocker, caplog): def test_validate_pairs_exception(default_conf, mocker, caplog):
@ -110,8 +193,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
def test_validate_pairs_stake_exception(default_conf, mocker, caplog): def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
conf = deepcopy(default_conf) default_conf['stake_currency'] = 'ETH'
conf['stake_currency'] = 'ETH'
api_mock = MagicMock() api_mock = MagicMock()
api_mock.name = MagicMock(return_value='binance') api_mock.name = MagicMock(return_value='binance')
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
@ -121,7 +203,7 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
OperationalException, OperationalException,
match=r'Pair ETH/BTC not compatible with stake_currency: ETH' match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
): ):
Exchange(conf) Exchange(default_conf)
def test_validate_timeframes(default_conf, mocker): def test_validate_timeframes(default_conf, mocker):

View File

@ -3,7 +3,6 @@
import json import json
import math import math
import random import random
from copy import deepcopy
from typing import List from typing import List
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -146,7 +145,7 @@ def _trend(signals, buy_value, sell_value):
return signals return signals
def _trend_alternate(dataframe=None): def _trend_alternate(dataframe=None, metadata=None):
signals = dataframe signals = dataframe
low = signals['low'] low = signals['low']
n = len(low) n = len(low)
@ -164,9 +163,6 @@ def _trend_alternate(dataframe=None):
# Unit tests # Unit tests
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@ -205,9 +201,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@ -276,15 +269,10 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
""" default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
Test setup_configuration() function
"""
conf = deepcopy(default_conf)
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf) read_data=json.dumps(default_conf)
)) ))
args = [ args = [
@ -298,9 +286,6 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog
def test_start(mocker, fee, default_conf, caplog) -> None: def test_start(mocker, fee, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
@ -323,25 +308,19 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
def test_backtesting_init(mocker, default_conf) -> None: def test_backtesting_init(mocker, default_conf) -> None:
"""
Test Backtesting._init() method
"""
patch_exchange(mocker) patch_exchange(mocker)
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf assert backtesting.config == default_conf
assert backtesting.ticker_interval == '5m' assert backtesting.ticker_interval == '5m'
assert callable(backtesting.tickerdata_to_dataframe) assert callable(backtesting.tickerdata_to_dataframe)
assert callable(backtesting.populate_buy_trend) assert callable(backtesting.advise_buy)
assert callable(backtesting.populate_sell_trend) assert callable(backtesting.advise_sell)
get_fee.assert_called() get_fee.assert_called()
assert backtesting.fee == 0.5 assert backtesting.fee == 0.5
def test_tickerdata_to_dataframe(default_conf, mocker) -> None: def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
"""
Test Backtesting.tickerdata_to_dataframe() method
"""
patch_exchange(mocker) patch_exchange(mocker)
timerange = TimeRange(None, 'line', 0, -100) timerange = TimeRange(None, 'line', 0, -100)
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
@ -358,9 +337,6 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
def test_get_timeframe(default_conf, mocker) -> None: def test_get_timeframe(default_conf, mocker) -> None:
"""
Test Backtesting.get_timeframe() method
"""
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -377,9 +353,6 @@ def test_get_timeframe(default_conf, mocker) -> None:
def test_generate_text_table(default_conf, mocker): def test_generate_text_table(default_conf, mocker):
"""
Test Backtesting.generate_text_table() method
"""
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -408,9 +381,6 @@ def test_generate_text_table(default_conf, mocker):
def test_generate_text_table_sell_reason(default_conf, mocker): def test_generate_text_table_sell_reason(default_conf, mocker):
"""
Test Backtesting.generate_text_table_sell_reason() method
"""
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -437,10 +407,6 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
def test_backtesting_start(default_conf, mocker, caplog) -> None: def test_backtesting_start(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method
"""
def get_timeframe(input1, input2): def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
@ -454,15 +420,14 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
get_timeframe=get_timeframe, get_timeframe=get_timeframe,
) )
conf = deepcopy(default_conf) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = 1
conf['ticker_interval'] = 1 default_conf['live'] = False
conf['live'] = False default_conf['datadir'] = None
conf['datadir'] = None default_conf['export'] = None
conf['export'] = None default_conf['timerange'] = '-100'
conf['timerange'] = '-100'
backtesting = Backtesting(conf) backtesting = Backtesting(default_conf)
backtesting.start() backtesting.start()
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
exists = [ exists = [
@ -477,10 +442,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method if no data is found
"""
def get_timeframe(input1, input2): def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
@ -494,15 +455,14 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
get_timeframe=get_timeframe, get_timeframe=get_timeframe,
) )
conf = deepcopy(default_conf) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = "1m"
conf['ticker_interval'] = "1m" default_conf['live'] = False
conf['live'] = False default_conf['datadir'] = None
conf['datadir'] = None default_conf['export'] = None
conf['export'] = None default_conf['timerange'] = '20180101-20180102'
conf['timerange'] = '20180101-20180102'
backtesting = Backtesting(conf) backtesting = Backtesting(default_conf)
backtesting.start() backtesting.start()
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
@ -510,9 +470,6 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
def test_backtest(default_conf, fee, mocker) -> None: def test_backtest(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method
"""
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -560,9 +517,6 @@ def test_backtest(default_conf, fee, mocker) -> None:
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method with 1 min ticker
"""
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -583,9 +537,6 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
def test_processed(default_conf, mocker) -> None: def test_processed(default_conf, mocker) -> None:
"""
Test Backtesting.backtest() method with offline data
"""
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -611,42 +562,42 @@ def test_backtest_ticks(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
ticks = [1, 5] ticks = [1, 5]
fun = Backtesting(default_conf).populate_buy_trend fun = Backtesting(default_conf).advise_buy
for _ in ticks: for _ in ticks:
backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override backtesting.advise_buy = fun # Override
backtesting.populate_sell_trend = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert not results.empty assert not results.empty
def test_backtest_clash_buy_sell(mocker, default_conf): def test_backtest_clash_buy_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None, pair=None):
buy_value = 1 buy_value = 1
sell_value = 1 sell_value = 1
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override backtesting.advise_buy = fun # Override
backtesting.populate_sell_trend = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert results.empty assert results.empty
def test_backtest_only_sell(mocker, default_conf): def test_backtest_only_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None, pair=None):
buy_value = 0 buy_value = 0
sell_value = 1 sell_value = 1
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override backtesting.advise_buy = fun # Override
backtesting.populate_sell_trend = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert results.empty assert results.empty
@ -655,8 +606,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC') backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override backtesting.advise_buy = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override backtesting.advise_sell = _trend_alternate # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
backtesting._store_backtest_result("test_.json", results) backtesting._store_backtest_result("test_.json", results)
assert len(results) == 4 assert len(results) == 4
@ -725,15 +676,14 @@ def test_backtest_record(default_conf, fee, mocker):
def test_backtest_start_live(default_conf, mocker, caplog): def test_backtest_start_live(default_conf, mocker, caplog):
conf = deepcopy(default_conf) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', mocker.patch('freqtrade.exchange.Exchange.get_ticker_history',
new=lambda s, n, i: _load_pair_as_ticks(n, i)) new=lambda s, n, i: _load_pair_as_ticks(n, i))
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf) read_data=json.dumps(default_conf)
)) ))
args = MagicMock() args = MagicMock()

View File

@ -1,6 +1,5 @@
# pragma pylint: disable=missing-docstring,W0212,C0103 # pragma pylint: disable=missing-docstring,W0212,C0103
import os import os
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pandas as pd import pandas as pd
@ -12,29 +11,22 @@ from freqtrade.strategy.resolver import StrategyResolver
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import log_has, patch_exchange
from freqtrade.tests.optimize.test_backtesting import get_args from freqtrade.tests.optimize.test_backtesting import get_args
# Avoid to reinit the same object again and again
_HYPEROPT_INITIALIZED = False
_HYPEROPT = None
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def init_hyperopt(default_conf, mocker): def hyperopt(default_conf, mocker):
global _HYPEROPT_INITIALIZED, _HYPEROPT patch_exchange(mocker)
if not _HYPEROPT_INITIALIZED: return Hyperopt(default_conf)
patch_exchange(mocker)
_HYPEROPT = Hyperopt(default_conf)
_HYPEROPT_INITIALIZED = True
# Functions for recurrent object patching # Functions for recurrent object patching
def create_trials(mocker) -> None: def create_trials(mocker, hyperopt) -> None:
""" """
When creating trials, mock the hyperopt Trials so that *by default* When creating trials, mock the hyperopt Trials so that *by default*
- we don't create any pickle'd files in the filesystem - we don't create any pickle'd files in the filesystem
- we might have a pickle'd file so make sure that we return - we might have a pickle'd file so make sure that we return
false when looking for it false when looking for it
""" """
_HYPEROPT.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') hyperopt.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False) mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1) mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
@ -45,9 +37,6 @@ def create_trials(mocker) -> None:
def test_start(mocker, default_conf, caplog) -> None: def test_start(mocker, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch( mocker.patch(
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
@ -76,8 +65,7 @@ def test_start(mocker, default_conf, caplog) -> None:
assert start_mock.call_count == 1 assert start_mock.call_count == 1
def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None: def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
hyperopt = _HYPEROPT
StrategyResolver({'strategy': 'DefaultStrategy'}) StrategyResolver({'strategy': 'DefaultStrategy'})
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
@ -87,17 +75,13 @@ def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
assert under > correct assert under > correct
def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None: def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None:
hyperopt = _HYPEROPT
shorter = hyperopt.calculate_loss(1, 100, 20) shorter = hyperopt.calculate_loss(1, 100, 20)
longer = hyperopt.calculate_loss(1, 100, 30) longer = hyperopt.calculate_loss(1, 100, 30)
assert shorter < longer assert shorter < longer
def test_loss_calculation_has_limited_profit(init_hyperopt) -> None: def test_loss_calculation_has_limited_profit(hyperopt) -> None:
hyperopt = _HYPEROPT
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20)
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
@ -105,8 +89,7 @@ def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
assert under > correct assert under > correct
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None: def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2 hyperopt.current_best_loss = 2
hyperopt.log_results( hyperopt.log_results(
{ {
@ -117,11 +100,10 @@ def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
} }
) )
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert ' 1/2: foo. Loss 1.00000'in out assert ' 1/2: foo. Loss 1.00000' in out
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None: def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2 hyperopt.current_best_loss = 2
hyperopt.log_results( hyperopt.log_results(
{ {
@ -131,13 +113,10 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
assert caplog.record_tuples == [] assert caplog.record_tuples == []
def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None: def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
trials = create_trials(mocker) trials = create_trials(mocker, hyperopt)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
hyperopt.trials = trials
hyperopt = _HYPEROPT
_HYPEROPT.trials = trials
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')
@ -148,11 +127,9 @@ def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
mock_dump.assert_called_once() mock_dump.assert_called_once()
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None: def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
trials = create_trials(mocker) trials = create_trials(mocker, hyperopt)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials) mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
hyperopt = _HYPEROPT
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( assert log_has(
@ -163,7 +140,7 @@ def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
mock_load.assert_called_once() mock_load.assert_called_once()
def test_roi_table_generation(init_hyperopt) -> None: def test_roi_table_generation(hyperopt) -> None:
params = { params = {
'roi_t1': 5, 'roi_t1': 5,
'roi_t2': 10, 'roi_t2': 10,
@ -173,11 +150,10 @@ def test_roi_table_generation(init_hyperopt) -> None:
'roi_p3': 3, 'roi_p3': 3,
} }
hyperopt = _HYPEROPT
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> None: def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1)) mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1))
@ -187,13 +163,12 @@ def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> N
) )
patch_exchange(mocker) patch_exchange(mocker)
conf = deepcopy(default_conf) default_conf.update({'config': 'config.json.example'})
conf.update({'config': 'config.json.example'}) default_conf.update({'epochs': 1})
conf.update({'epochs': 1}) default_conf.update({'timerange': None})
conf.update({'timerange': None}) default_conf.update({'spaces': 'all'})
conf.update({'spaces': 'all'})
hyperopt = Hyperopt(conf) hyperopt = Hyperopt(default_conf)
hyperopt.tickerdata_to_dataframe = MagicMock() hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start() hyperopt.start()
@ -203,11 +178,7 @@ def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> N
assert dumper.called assert dumper.called
def test_format_results(init_hyperopt): def test_format_results(hyperopt):
"""
Test Hyperopt.format_results()
"""
# Test with BTC as stake_currency # Test with BTC as stake_currency
trades = [ trades = [
('ETH/BTC', 2, 2, 123), ('ETH/BTC', 2, 2, 123),
@ -217,7 +188,7 @@ def test_format_results(init_hyperopt):
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration'] labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
df = pd.DataFrame.from_records(trades, columns=labels) df = pd.DataFrame.from_records(trades, columns=labels)
result = _HYPEROPT.format_results(df) result = hyperopt.format_results(df)
assert result.find(' 66.67%') assert result.find(' 66.67%')
assert result.find('Total profit 1.00000000 BTC') assert result.find('Total profit 1.00000000 BTC')
assert result.find('2.0000Σ %') assert result.find('2.0000Σ %')
@ -229,25 +200,25 @@ def test_format_results(init_hyperopt):
('XPR/EUR', -1, -2, -246) ('XPR/EUR', -1, -2, -246)
] ]
df = pd.DataFrame.from_records(trades, columns=labels) df = pd.DataFrame.from_records(trades, columns=labels)
result = _HYPEROPT.format_results(df) result = hyperopt.format_results(df)
assert result.find('Total profit 1.00000000 EUR') assert result.find('Total profit 1.00000000 EUR')
def test_has_space(init_hyperopt): def test_has_space(hyperopt):
_HYPEROPT.config.update({'spaces': ['buy', 'roi']}) hyperopt.config.update({'spaces': ['buy', 'roi']})
assert _HYPEROPT.has_space('roi') assert hyperopt.has_space('roi')
assert _HYPEROPT.has_space('buy') assert hyperopt.has_space('buy')
assert not _HYPEROPT.has_space('stoploss') assert not hyperopt.has_space('stoploss')
_HYPEROPT.config.update({'spaces': ['all']}) hyperopt.config.update({'spaces': ['all']})
assert _HYPEROPT.has_space('buy') assert hyperopt.has_space('buy')
def test_populate_indicators(init_hyperopt) -> None: 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': tick} tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
# Check if some indicators are generated. We will not test all of them # Check if some indicators are generated. We will not test all of them
assert 'adx' in dataframe assert 'adx' in dataframe
@ -255,13 +226,13 @@ def test_populate_indicators(init_hyperopt) -> None:
assert 'rsi' in dataframe assert 'rsi' in dataframe
def test_buy_strategy_generator(init_hyperopt) -> None: 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': tick} tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
populate_buy_trend = _HYPEROPT.buy_strategy_generator( populate_buy_trend = hyperopt.buy_strategy_generator(
{ {
'adx-value': 20, 'adx-value': 20,
'fastd-value': 20, 'fastd-value': 20,
@ -274,17 +245,16 @@ def test_buy_strategy_generator(init_hyperopt) -> None:
'trigger': 'bb_lower' 'trigger': 'bb_lower'
} }
) )
result = populate_buy_trend(dataframe) result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'})
# Check if some indicators are generated. We will not test all of them # Check if some indicators are generated. We will not test all of them
assert 'buy' in result assert 'buy' in result
assert 1 in result['buy'] assert 1 in result['buy']
def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: def test_generate_optimizer(mocker, default_conf) -> None:
conf = deepcopy(default_conf) default_conf.update({'config': 'config.json.example'})
conf.update({'config': 'config.json.example'}) default_conf.update({'timerange': None})
conf.update({'timerange': None}) default_conf.update({'spaces': 'all'})
conf.update({'spaces': 'all'})
trades = [ trades = [
('POWR/BTC', 0.023117, 0.000233, 100) ('POWR/BTC', 0.023117, 0.000233, 100)
@ -324,6 +294,6 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
'params': optimizer_param 'params': optimizer_param
} }
hyperopt = Hyperopt(conf) hyperopt = Hyperopt(default_conf)
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
assert generate_optimizer_value == response_expected assert generate_optimizer_value == response_expected

View File

@ -11,8 +11,8 @@ from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import RPC, RPCException from freqtrade.rpc import RPC, RPCException
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap, from freqtrade.tests.test_freqtradebot import patch_get_signal
patch_get_signal) from freqtrade.tests.conftest import patch_coinmarketcap
# Functions for recurrent object patching # Functions for recurrent object patching
@ -278,9 +278,6 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
def test_rpc_balance_handle(default_conf, mocker): def test_rpc_balance_handle(default_conf, mocker):
"""
Test rpc_balance() method
"""
mock_balance = { mock_balance = {
'BTC': { 'BTC': {
'free': 10.0, 'free': 10.0,

View File

@ -1,7 +1,6 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import logging import logging
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.rpc import RPCMessageType, RPCManager from freqtrade.rpc import RPCMessageType, RPCManager
@ -9,18 +8,16 @@ from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
def test__init__(mocker, default_conf) -> None: def test__init__(mocker, default_conf) -> None:
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
@ -40,10 +37,9 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.cleanup() rpc_manager.cleanup()
@ -70,10 +66,9 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_disabled(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())
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg({ rpc_manager.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION, 'type': RPCMessageType.STATUS_NOTIFICATION,
@ -101,10 +96,9 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
def test_init_webhook_disabled(mocker, default_conf, caplog) -> None: def test_init_webhook_disabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False default_conf['webhook'] = {'enabled': False}
conf['webhook'] = {'enabled': False} rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples) assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples)
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []

View File

@ -3,7 +3,6 @@
# pragma pylint: disable=too-many-lines, too-many-arguments # pragma pylint: disable=too-many-lines, too-many-arguments
import re import re
from copy import deepcopy
from datetime import datetime from datetime import datetime
from random import randint from random import randint
from unittest.mock import MagicMock, ANY from unittest.mock import MagicMock, ANY
@ -21,8 +20,8 @@ from freqtrade.rpc.telegram import Telegram, authorized_only
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
patch_exchange) patch_exchange)
from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap, from freqtrade.tests.test_freqtradebot import patch_get_signal
patch_get_signal) from freqtrade.tests.conftest import patch_coinmarketcap
class DummyCls(Telegram): class DummyCls(Telegram):
@ -96,9 +95,8 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
update = Update(randint(1, 100)) update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf)
bot = FreqtradeBot(conf)
patch_get_signal(bot, (True, False)) patch_get_signal(bot, (True, False))
dummy = DummyCls(bot) dummy = DummyCls(bot)
dummy.dummy_handler(bot=MagicMock(), update=update) dummy.dummy_handler(bot=MagicMock(), update=update)
@ -124,9 +122,8 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
update = Update(randint(1, 100)) update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf)
bot = FreqtradeBot(conf)
patch_get_signal(bot, (True, False)) patch_get_signal(bot, (True, False))
dummy = DummyCls(bot) dummy = DummyCls(bot)
dummy.dummy_handler(bot=MagicMock(), update=update) dummy.dummy_handler(bot=MagicMock(), update=update)
@ -152,10 +149,9 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
update = Update(randint(1, 100)) update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False
bot = FreqtradeBot(conf) bot = FreqtradeBot(default_conf)
patch_get_signal(bot, (True, False)) patch_get_signal(bot, (True, False))
dummy = DummyCls(bot) dummy = DummyCls(bot)
@ -177,9 +173,8 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
update.message.chat.id = 123 update.message.chat.id = 123
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = 123
conf['telegram']['chat_id'] = 123
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
@ -214,7 +209,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False)) patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
@ -294,9 +289,8 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
conf = deepcopy(default_conf) default_conf['stake_amount'] = 15.0
conf['stake_amount'] = 15.0 freqtradebot = FreqtradeBot(default_conf)
freqtradebot = FreqtradeBot(conf)
patch_get_signal(freqtradebot, (True, False)) patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
@ -1181,9 +1175,8 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
def test__send_msg(default_conf, mocker) -> None: def test__send_msg(default_conf, mocker) -> None:
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
conf = deepcopy(default_conf)
bot = MagicMock() bot = MagicMock()
freqtradebot = get_patched_freqtradebot(mocker, conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
@ -1194,10 +1187,9 @@ def test__send_msg(default_conf, mocker) -> None:
def test__send_msg_network_error(default_conf, mocker, caplog) -> None: def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
conf = deepcopy(default_conf)
bot = MagicMock() bot = MagicMock()
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
freqtradebot = get_patched_freqtradebot(mocker, conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True

View File

@ -0,0 +1,235 @@
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy # noqa
# This class is a sample. Feel free to customize it.
class TestStrategyLegacy(IStrategy):
"""
This is a test strategy using the legacy function headers, which will be
removed in a future update.
Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py
for a uptodate version of this template.
"""
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.10
# Optimal ticker interval for the strategy
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
"""
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
"""
# Awesome oscillator
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
dataframe['cci'] = ta.CCI(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# Minus Directional Indicator / Movement
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# ROC
dataframe['roc'] = ta.ROC(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
"""
# Overlap Studies
# ------------------------------------
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
"""
# EMA - Exponential Moving Average
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
"""
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
"""
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
"""
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
"""
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 30) &
(dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1))
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 70) &
(dataframe['tema'] > dataframe['bb_middleband']) &
(dataframe['tema'] < dataframe['tema'].shift(1))
),
'sell'] = 1
return dataframe

View File

@ -25,10 +25,11 @@ def test_default_strategy_structure():
def test_default_strategy(result): def test_default_strategy(result):
strategy = DefaultStrategy({}) strategy = DefaultStrategy({})
metadata = {'pair': 'ETH/BTC'}
assert type(strategy.minimal_roi) is dict assert type(strategy.minimal_roi) is dict
assert type(strategy.stoploss) is float assert type(strategy.stoploss) is float
assert type(strategy.ticker_interval) is str assert type(strategy.ticker_interval) is str
indicators = strategy.populate_indicators(result) indicators = strategy.populate_indicators(result, metadata)
assert type(indicators) is DataFrame assert type(indicators) is DataFrame
assert type(strategy.populate_buy_trend(indicators)) is DataFrame assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
assert type(strategy.populate_sell_trend(indicators)) is DataFrame assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame

View File

@ -98,9 +98,6 @@ def test_get_signal_handles_exceptions(mocker, default_conf):
def test_tickerdata_to_dataframe(default_conf) -> None: def test_tickerdata_to_dataframe(default_conf) -> None:
"""
Test Analyze.tickerdata_to_dataframe() method
"""
strategy = DefaultStrategy(default_conf) strategy = DefaultStrategy(default_conf)
timerange = TimeRange(None, 'line', 0, -100) timerange = TimeRange(None, 'line', 0, -100)

View File

@ -1,8 +1,10 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103 # pragma pylint: disable=missing-docstring, protected-access, C0103
import logging import logging
import os from os import path
import warnings
import pytest import pytest
from pandas import DataFrame
from freqtrade.strategy import import_strategy from freqtrade.strategy import import_strategy
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
@ -37,8 +39,8 @@ def test_import_strategy(caplog):
def test_search_strategy(): def test_search_strategy():
default_config = {} default_config = {}
default_location = os.path.join(os.path.dirname( default_location = path.join(path.dirname(
os.path.realpath(__file__)), '..', '..', 'strategy' path.realpath(__file__)), '..', '..', 'strategy'
) )
assert isinstance( assert isinstance(
StrategyResolver._search_strategy( StrategyResolver._search_strategy(
@ -57,12 +59,13 @@ def test_search_strategy():
def test_load_strategy(result): def test_load_strategy(result):
resolver = StrategyResolver({'strategy': 'TestStrategy'}) resolver = StrategyResolver({'strategy': 'TestStrategy'})
assert 'adx' in resolver.strategy.populate_indicators(result) metadata = {'pair': 'ETH/BTC'}
assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata)
def test_load_strategy_invalid_directory(result, caplog): def test_load_strategy_invalid_directory(result, caplog):
resolver = StrategyResolver() resolver = StrategyResolver()
extra_dir = os.path.join('some', 'path') extra_dir = path.join('some', 'path')
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
assert ( assert (
@ -70,7 +73,8 @@ def test_load_strategy_invalid_directory(result, caplog):
logging.WARNING, logging.WARNING,
'Path "{}" does not exist'.format(extra_dir), 'Path "{}" does not exist'.format(extra_dir),
) in caplog.record_tuples ) in caplog.record_tuples
assert 'adx' in resolver.strategy.populate_indicators(result)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_not_found_strategy(): def test_load_not_found_strategy():
@ -85,7 +89,7 @@ def test_strategy(result):
config = {'strategy': 'DefaultStrategy'} config = {'strategy': 'DefaultStrategy'}
resolver = StrategyResolver(config) resolver = StrategyResolver(config)
metadata = {'pair': 'ETH/BTC'}
assert resolver.strategy.minimal_roi[0] == 0.04 assert resolver.strategy.minimal_roi[0] == 0.04
assert config["minimal_roi"]['0'] == 0.04 assert config["minimal_roi"]['0'] == 0.04
@ -95,12 +99,13 @@ def test_strategy(result):
assert resolver.strategy.ticker_interval == '5m' assert resolver.strategy.ticker_interval == '5m'
assert config['ticker_interval'] == '5m' assert config['ticker_interval'] == '5m'
assert 'adx' in resolver.strategy.populate_indicators(result) df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata)
assert 'adx' in df_indicators
dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result)) dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata)
assert 'buy' in dataframe.columns assert 'buy' in dataframe.columns
dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result)) dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata)
assert 'sell' in dataframe.columns assert 'sell' in dataframe.columns
@ -150,3 +155,59 @@ def test_strategy_override_ticker_interval(caplog):
logging.INFO, logging.INFO,
'Override strategy \'ticker_interval\' with value in config file: 60.' 'Override strategy \'ticker_interval\' with value in config file: 60.'
) in caplog.record_tuples ) in caplog.record_tuples
def test_deprecate_populate_indicators(result):
default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location})
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC')
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \
in str(w[-1].message)
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
resolver.strategy.advise_buy(indicators, 'ETH/BTC')
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \
in str(w[-1].message)
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
resolver.strategy.advise_sell(indicators, 'ETH_BTC')
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \
in str(w[-1].message)
def test_call_deprecated_function(result, monkeypatch):
default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location})
metadata = {'pair': 'ETH/BTC'}
# Make sure we are using a legacy function
assert resolver.strategy._populate_fun_len == 2
assert resolver.strategy._buy_fun_len == 2
assert resolver.strategy._sell_fun_len == 2
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
assert type(indicator_df) is DataFrame
assert 'adx' in indicator_df.columns
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
assert type(buydf) is DataFrame
assert 'buy' in buydf.columns
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
assert type(selldf) is DataFrame
assert 'sell' in selldf

View File

@ -1,9 +1,5 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
"""
Unit test file for arguments.py
"""
import argparse import argparse
import pytest import pytest

View File

@ -2,13 +2,13 @@
import json import json
from argparse import Namespace from argparse import Namespace
from copy import deepcopy
import logging import logging
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import ValidationError from jsonschema import validate, ValidationError
from freqtrade import constants
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration, set_loggers from freqtrade.configuration import Configuration, set_loggers
@ -17,30 +17,27 @@ from freqtrade.tests.conftest import log_has
def test_load_config_invalid_pair(default_conf) -> None: def test_load_config_invalid_pair(default_conf) -> None:
conf = deepcopy(default_conf) default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
conf['exchange']['pair_whitelist'].append('ETH-BTC')
with pytest.raises(ValidationError, match=r'.*does not match.*'): with pytest.raises(ValidationError, match=r'.*does not match.*'):
configuration = Configuration(Namespace()) configuration = Configuration(Namespace())
configuration._validate_config(conf) configuration._validate_config(default_conf)
def test_load_config_missing_attributes(default_conf) -> None: def test_load_config_missing_attributes(default_conf) -> None:
conf = deepcopy(default_conf) default_conf.pop('exchange')
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.*'):
configuration = Configuration(Namespace()) configuration = Configuration(Namespace())
configuration._validate_config(conf) configuration._validate_config(default_conf)
def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_incorrect_stake_amount(default_conf) -> None:
conf = deepcopy(default_conf) default_conf['stake_amount'] = 'fake'
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\'.*'):
configuration = Configuration(Namespace()) configuration = Configuration(Namespace())
configuration._validate_config(conf) configuration._validate_config(default_conf)
def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None:
@ -57,10 +54,9 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
conf = deepcopy(default_conf) default_conf['max_open_trades'] = 0
conf['max_open_trades'] = 0
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf) read_data=json.dumps(default_conf)
)) ))
Configuration(Namespace())._load_config_file('somefile') Configuration(Namespace())._load_config_file('somefile')
@ -151,13 +147,12 @@ def test_load_config_with_params(default_conf, mocker) -> None:
def test_load_custom_strategy(default_conf, mocker) -> None: def test_load_custom_strategy(default_conf, mocker) -> None:
custom_conf = deepcopy(default_conf) default_conf.update({
custom_conf.update({
'strategy': 'CustomStrategy', 'strategy': 'CustomStrategy',
'strategy_path': '/tmp/strategies', 'strategy_path': '/tmp/strategies',
}) })
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(custom_conf) read_data=json.dumps(default_conf)
)) ))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
@ -322,29 +317,25 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
def test_check_exchange(default_conf) -> None: def test_check_exchange(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
configuration = Configuration(Namespace()) configuration = Configuration(Namespace())
# Test a valid exchange # Test a valid exchange
conf.get('exchange').update({'name': 'BITTREX'}) default_conf.get('exchange').update({'name': 'BITTREX'})
assert configuration.check_exchange(conf) assert configuration.check_exchange(default_conf)
# Test a valid exchange # Test a valid exchange
conf.get('exchange').update({'name': 'binance'}) default_conf.get('exchange').update({'name': 'binance'})
assert configuration.check_exchange(conf) assert configuration.check_exchange(default_conf)
# Test a invalid exchange # Test a invalid exchange
conf.get('exchange').update({'name': 'unknown_exchange'}) default_conf.get('exchange').update({'name': 'unknown_exchange'})
configuration.config = conf configuration.config = default_conf
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
match=r'.*Exchange "unknown_exchange" not supported.*' match=r'.*Exchange "unknown_exchange" not supported.*'
): ):
configuration.check_exchange(conf) configuration.check_exchange(default_conf)
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
@ -398,3 +389,7 @@ def test_set_loggers() -> None:
assert logging.getLogger('requests').level is logging.DEBUG assert logging.getLogger('requests').level is logging.DEBUG
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
assert logging.getLogger('telegram').level is logging.INFO assert logging.getLogger('telegram').level is logging.INFO
def test_validate_default_conf(default_conf) -> None:
validate(default_conf, constants.CONF_SCHEMA)

View File

@ -14,7 +14,7 @@ def load_dataframe_pair(pairs, strategy):
assert isinstance(pairs[0], str) assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]] dataframe = ld[pairs[0]]
dataframe = strategy.analyze_ticker(dataframe) dataframe = strategy.analyze_ticker(dataframe, pairs[0])
return dataframe return dataframe

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
@ -23,46 +22,40 @@ def test_init_create_session(default_conf):
def test_init_custom_db_url(default_conf, mocker): def test_init_custom_db_url(default_conf, mocker):
conf = deepcopy(default_conf)
# Update path to a value other than default, but still in-memory # Update path to a value other than default, but still in-memory
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
init(conf) init(default_conf)
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
def test_init_invalid_db_url(default_conf): def test_init_invalid_db_url(default_conf):
conf = deepcopy(default_conf)
# Update path to a value other than default, but still in-memory # Update path to a value other than default, but still in-memory
conf.update({'db_url': 'unknown:///some.url'}) default_conf.update({'db_url': 'unknown:///some.url'})
with pytest.raises(OperationalException, match=r'.*no valid database URL*'): with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init(conf) init(default_conf)
def test_init_prod_db(default_conf, mocker): def test_init_prod_db(default_conf, mocker):
conf = deepcopy(default_conf) default_conf.update({'dry_run': False})
conf.update({'dry_run': False}) default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
init(conf) init(default_conf)
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
def test_init_dryrun_db(default_conf, mocker): def test_init_dryrun_db(default_conf, mocker):
conf = deepcopy(default_conf) default_conf.update({'dry_run': True})
conf.update({'dry_run': True}) default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
init(conf) init(default_conf)
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'

View File

@ -1,4 +1,4 @@
ccxt==1.17.45 ccxt==1.17.49
SQLAlchemy==1.2.10 SQLAlchemy==1.2.10
python-telegram-bot==10.1.0 python-telegram-bot==10.1.0
arrow==0.12.1 arrow==0.12.1

View File

@ -159,8 +159,8 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
dataframes = strategy.tickerdata_to_dataframe(tickers) dataframes = strategy.tickerdata_to_dataframe(tickers)
dataframe = dataframes[pair] dataframe = dataframes[pair]
dataframe = strategy.populate_buy_trend(dataframe) dataframe = strategy.advise_buy(dataframe, {'pair': pair})
dataframe = strategy.populate_sell_trend(dataframe) dataframe = strategy.advise_sell(dataframe, {'pair': pair})
if len(dataframe.index) > args.plot_limit: if len(dataframe.index) > args.plot_limit:
logger.warning('Ticker contained more than %s candles as defined ' logger.warning('Ticker contained more than %s candles as defined '

View File

@ -18,7 +18,7 @@ setup(name='freqtrade',
license='GPLv3', license='GPLv3',
packages=['freqtrade'], packages=['freqtrade'],
scripts=['bin/freqtrade'], scripts=['bin/freqtrade'],
setup_requires=['pytest-runner'], setup_requires=['pytest-runner', 'numpy'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[ install_requires=[
'ccxt', 'ccxt',

View File

@ -18,6 +18,7 @@ class TestStrategy(IStrategy):
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:
:return: a Dataframe with all mandatory indicators for the strategies
- Rename the class name (Do not forget to update class_name) - Rename the class name (Do not forget to update class_name)
- Add any methods you want to build your strategy - Add any methods you want to build your strategy
- Add any lib you need to build your strategy - Add any lib you need to build your strategy
@ -44,13 +45,16 @@ class TestStrategy(IStrategy):
# Optimal ticker interval for the strategy # Optimal ticker interval for the strategy
ticker_interval = '5m' ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
""" """
# Momentum Indicator # Momentum Indicator
@ -211,10 +215,11 @@ class TestStrategy(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
@ -227,10 +232,11 @@ class TestStrategy(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[