Merge branch 'develop' into ccxt-async
This commit is contained in:
commit
154e4569d7
@ -15,7 +15,8 @@ WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt /freqtrade/
|
||||
RUN pip install -r requirements.txt
|
||||
RUN pip install numpy \
|
||||
&& pip install -r requirements.txt
|
||||
|
||||
# Install and execute
|
||||
COPY . /freqtrade/
|
||||
|
@ -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)
|
||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.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)
|
||||
- [Bot commands](#bot-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)
|
||||
- [Min hardware required](#min-hardware-required)
|
||||
- [Software requirements](#software-requirements)
|
||||
|
||||
|
||||
## Quick start
|
||||
|
||||
|
@ -39,7 +39,6 @@ A strategy file contains all the information needed to build a good strategy:
|
||||
- Sell strategy rules
|
||||
- Minimal ROI recommended
|
||||
- Stoploss recommended
|
||||
- Hyperopt parameter
|
||||
|
||||
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`
|
||||
@ -61,22 +60,22 @@ file as reference.**
|
||||
|
||||
### Buy strategy
|
||||
|
||||
Edit the method `populate_buy_trend()` into your strategy file to
|
||||
update your buy strategy.
|
||||
Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy.
|
||||
|
||||
Sample from `user_data/strategies/test_strategy.py`:
|
||||
|
||||
```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
|
||||
:param dataframe: DataFrame
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['tema'] <= dataframe['blower']) &
|
||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||
),
|
||||
'buy'] = 1
|
||||
@ -87,38 +86,47 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
### 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`:
|
||||
|
||||
```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
|
||||
:param dataframe: DataFrame
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['tema'] > dataframe['blower']) &
|
||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
||||
```
|
||||
|
||||
## Add more Indicator
|
||||
## Add more Indicators
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```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
|
||||
|
||||
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['adx'] = ta.ADX(dataframe)
|
||||
@ -149,6 +157,11 @@ def populate_indicators(dataframe: DataFrame) -> 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
|
||||
|
||||
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
||||
|
@ -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)
|
||||
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.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))
|
||||
|
@ -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`.
|
||||
|
||||
|
||||
## Manual installation - Linux/MacOS
|
||||
|
||||
The following steps are made for Linux/MacOS environment
|
||||
|
||||
**1. Clone the repo**
|
||||
### 1. Clone the repo
|
||||
|
||||
```bash
|
||||
git clone git@github.com:freqtrade/freqtrade.git
|
||||
git checkout develop
|
||||
cd freqtrade
|
||||
```
|
||||
**2. Create the config file**
|
||||
|
||||
### 2. Create the config file
|
||||
|
||||
Switch `"dry_run": true,`
|
||||
|
||||
```bash
|
||||
cp config.json.example config.json
|
||||
vi config.json
|
||||
```
|
||||
**3. Build your docker image and run it**
|
||||
|
||||
### 3. Build your docker image and run it
|
||||
|
||||
```bash
|
||||
docker build -t 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
151
docs/sandbox-testing.md
Normal 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
|
||||
```
|
@ -125,6 +125,7 @@ CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'sandbox': {'type': 'boolean'},
|
||||
'key': {'type': 'string'},
|
||||
'secret': {'type': 'string'},
|
||||
'password': {'type': 'string'},
|
||||
|
@ -4,6 +4,7 @@ import logging
|
||||
from random import randint
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from math import floor, ceil
|
||||
|
||||
import ccxt
|
||||
import ccxt.async_support as ccxt_async
|
||||
@ -100,6 +101,8 @@ class Exchange(object):
|
||||
except (KeyError, AttributeError):
|
||||
raise OperationalException(f'Exchange {name} is not supported')
|
||||
|
||||
self.set_sandbox(api, exchange_config, name)
|
||||
|
||||
return api
|
||||
|
||||
@property
|
||||
@ -112,6 +115,16 @@ class Exchange(object):
|
||||
"""exchange ccxt 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:
|
||||
"""
|
||||
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]
|
||||
|
||||
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:
|
||||
if self._conf['dry_run']:
|
||||
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||
@ -172,6 +207,10 @@ class Exchange(object):
|
||||
return {'id': order_id}
|
||||
|
||||
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)
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
@ -205,6 +244,10 @@ class Exchange(object):
|
||||
return {'id': order_id}
|
||||
|
||||
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)
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
|
@ -57,8 +57,8 @@ class Backtesting(object):
|
||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||
self.ticker_interval = self.strategy.ticker_interval
|
||||
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
|
||||
self.populate_buy_trend = self.strategy.populate_buy_trend
|
||||
self.populate_sell_trend = self.strategy.populate_sell_trend
|
||||
self.advise_buy = self.strategy.advise_buy
|
||||
self.advise_sell = self.strategy.advise_sell
|
||||
|
||||
# Reset keys for backtesting
|
||||
self.config['exchange']['key'] = ''
|
||||
@ -229,8 +229,8 @@ class Backtesting(object):
|
||||
for pair, pair_data in processed.items():
|
||||
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
||||
|
||||
ticker_data = self.populate_sell_trend(
|
||||
self.populate_buy_trend(pair_data))[headers].copy()
|
||||
ticker_data = self.advise_sell(
|
||||
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
|
||||
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
|
||||
|
@ -75,7 +75,7 @@ class Hyperopt(Backtesting):
|
||||
return arg_dict
|
||||
|
||||
@staticmethod
|
||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
@ -228,7 +228,7 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
@ -270,7 +270,7 @@ class Hyperopt(Backtesting):
|
||||
self.strategy.minimal_roi = self.generate_roi_table(params)
|
||||
|
||||
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'):
|
||||
self.strategy.stoploss = params['stoploss']
|
||||
@ -351,7 +351,7 @@ class Hyperopt(Backtesting):
|
||||
)
|
||||
|
||||
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)
|
||||
self.exchange = None # type: ignore
|
||||
self.load_previous_results()
|
||||
@ -360,7 +360,7 @@ class Hyperopt(Backtesting):
|
||||
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
|
||||
|
||||
opt = self.get_optimizer(cpus)
|
||||
EVALS = max(self.total_tries//cpus, 1)
|
||||
EVALS = max(self.total_tries // cpus, 1)
|
||||
try:
|
||||
with Parallel(n_jobs=cpus) as parallel:
|
||||
for i in range(EVALS):
|
||||
|
@ -28,13 +28,16 @@ class DefaultStrategy(IStrategy):
|
||||
# Optimal ticker interval for the strategy
|
||||
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
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
@ -196,10 +199,11 @@ class DefaultStrategy(IStrategy):
|
||||
|
||||
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
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
@ -217,10 +221,11 @@ class DefaultStrategy(IStrategy):
|
||||
|
||||
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
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
|
@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, List, NamedTuple, Tuple
|
||||
import warnings
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
@ -57,34 +58,45 @@ class IStrategy(ABC):
|
||||
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
|
||||
|
||||
# associated stoploss
|
||||
stoploss: float
|
||||
|
||||
# associated ticker interval
|
||||
ticker_interval: str
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
self.config = config
|
||||
|
||||
@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
|
||||
: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
|
||||
"""
|
||||
|
||||
@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
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
|
||||
@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
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with sell column
|
||||
"""
|
||||
|
||||
@ -94,16 +106,16 @@ class IStrategy(ABC):
|
||||
"""
|
||||
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
|
||||
add several TA indicators and buy signal to it
|
||||
:return DataFrame with ticker data and indicator data
|
||||
"""
|
||||
dataframe = parse_ticker_dataframe(ticker_history)
|
||||
dataframe = self.populate_indicators(dataframe)
|
||||
dataframe = self.populate_buy_trend(dataframe)
|
||||
dataframe = self.populate_sell_trend(dataframe)
|
||||
dataframe = self.advise_indicators(dataframe, metadata)
|
||||
dataframe = self.advise_buy(dataframe, metadata)
|
||||
dataframe = self.advise_sell(dataframe, metadata)
|
||||
return dataframe
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
dataframe = self.analyze_ticker(ticker_hist)
|
||||
dataframe = self.analyze_ticker(ticker_hist, {'pair': pair})
|
||||
except ValueError as error:
|
||||
logger.warning(
|
||||
'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
|
||||
"""
|
||||
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()}
|
||||
|
||||
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)
|
||||
|
@ -92,6 +92,13 @@ class StrategyResolver(object):
|
||||
strategy = self._search_strategy(path, strategy_name=strategy_name, config=config)
|
||||
if strategy:
|
||||
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)
|
||||
except FileNotFoundError:
|
||||
logger.warning('Path "%s" does not exist', path)
|
||||
|
@ -8,10 +8,8 @@ from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from jsonschema import validate
|
||||
from telegram import Chat, Message, Update
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
@ -127,7 +125,6 @@ def default_conf():
|
||||
"db_url": "sqlite://",
|
||||
"loglevel": logging.DEBUG,
|
||||
}
|
||||
validate(configuration, constants.CONF_SCHEMA)
|
||||
return configuration
|
||||
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||
# pragma pylint: disable=protected-access
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from random import randint
|
||||
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):
|
||||
"""Function to test ccxt exception handling """
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
@ -52,6 +49,93 @@ def test_init_exception(default_conf, mocker):
|
||||
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):
|
||||
api_mock = MagicMock()
|
||||
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={
|
||||
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
|
||||
})
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_currency'] = 'ETH'
|
||||
default_conf['stake_currency'] = 'ETH'
|
||||
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'not compatible'):
|
||||
Exchange(conf)
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
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):
|
||||
caplog.set_level(logging.INFO)
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_currency'] = 'ETH'
|
||||
default_conf['stake_currency'] = 'ETH'
|
||||
api_mock = MagicMock()
|
||||
api_mock.name = MagicMock(return_value='binance')
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||
@ -121,7 +203,7 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
|
||||
OperationalException,
|
||||
match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
|
||||
):
|
||||
Exchange(conf)
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
def test_validate_timeframes(default_conf, mocker):
|
||||
|
@ -3,7 +3,6 @@
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
from copy import deepcopy
|
||||
from typing import List
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@ -146,7 +145,7 @@ def _trend(signals, buy_value, sell_value):
|
||||
return signals
|
||||
|
||||
|
||||
def _trend_alternate(dataframe=None):
|
||||
def _trend_alternate(dataframe=None, metadata=None):
|
||||
signals = dataframe
|
||||
low = signals['low']
|
||||
n = len(low)
|
||||
@ -164,9 +163,6 @@ def _trend_alternate(dataframe=None):
|
||||
|
||||
# Unit tests
|
||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
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:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
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:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(conf)
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
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:
|
||||
"""
|
||||
Test start() function
|
||||
"""
|
||||
start_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
@ -323,25 +308,19 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_backtesting_init(mocker, default_conf) -> None:
|
||||
"""
|
||||
Test Backtesting._init() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||
backtesting = Backtesting(default_conf)
|
||||
assert backtesting.config == default_conf
|
||||
assert backtesting.ticker_interval == '5m'
|
||||
assert callable(backtesting.tickerdata_to_dataframe)
|
||||
assert callable(backtesting.populate_buy_trend)
|
||||
assert callable(backtesting.populate_sell_trend)
|
||||
assert callable(backtesting.advise_buy)
|
||||
assert callable(backtesting.advise_sell)
|
||||
get_fee.assert_called()
|
||||
assert backtesting.fee == 0.5
|
||||
|
||||
|
||||
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test Backtesting.tickerdata_to_dataframe() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
timerange = TimeRange(None, 'line', 0, -100)
|
||||
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:
|
||||
"""
|
||||
Test Backtesting.get_timeframe() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
@ -377,9 +353,6 @@ def test_get_timeframe(default_conf, mocker) -> None:
|
||||
|
||||
|
||||
def test_generate_text_table(default_conf, mocker):
|
||||
"""
|
||||
Test Backtesting.generate_text_table() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
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):
|
||||
"""
|
||||
Test Backtesting.generate_text_table_sell_reason() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
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:
|
||||
"""
|
||||
Test Backtesting.start() method
|
||||
"""
|
||||
|
||||
def get_timeframe(input1, input2):
|
||||
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,
|
||||
)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
conf['ticker_interval'] = 1
|
||||
conf['live'] = False
|
||||
conf['datadir'] = None
|
||||
conf['export'] = None
|
||||
conf['timerange'] = '-100'
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
default_conf['ticker_interval'] = 1
|
||||
default_conf['live'] = False
|
||||
default_conf['datadir'] = None
|
||||
default_conf['export'] = None
|
||||
default_conf['timerange'] = '-100'
|
||||
|
||||
backtesting = Backtesting(conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.start()
|
||||
# check the logs, that will contain the backtest result
|
||||
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:
|
||||
"""
|
||||
Test Backtesting.start() method if no data is found
|
||||
"""
|
||||
|
||||
def get_timeframe(input1, input2):
|
||||
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,
|
||||
)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
conf['ticker_interval'] = "1m"
|
||||
conf['live'] = False
|
||||
conf['datadir'] = None
|
||||
conf['export'] = None
|
||||
conf['timerange'] = '20180101-20180102'
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
default_conf['ticker_interval'] = "1m"
|
||||
default_conf['live'] = False
|
||||
default_conf['datadir'] = None
|
||||
default_conf['export'] = None
|
||||
default_conf['timerange'] = '20180101-20180102'
|
||||
|
||||
backtesting = Backtesting(conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.start()
|
||||
# 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:
|
||||
"""
|
||||
Test Backtesting.backtest() method
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
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:
|
||||
"""
|
||||
Test Backtesting.backtest() method with 1 min ticker
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
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:
|
||||
"""
|
||||
Test Backtesting.backtest() method with offline data
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
@ -611,42 +562,42 @@ def test_backtest_ticks(default_conf, fee, mocker):
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
ticks = [1, 5]
|
||||
fun = Backtesting(default_conf).populate_buy_trend
|
||||
fun = Backtesting(default_conf).advise_buy
|
||||
for _ in ticks:
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.populate_buy_trend = fun # Override
|
||||
backtesting.populate_sell_trend = fun # Override
|
||||
backtesting.advise_buy = fun # Override
|
||||
backtesting.advise_sell = fun # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
assert not results.empty
|
||||
|
||||
|
||||
def test_backtest_clash_buy_sell(mocker, default_conf):
|
||||
# Override the default buy trend function in our default_strategy
|
||||
def fun(dataframe=None):
|
||||
def fun(dataframe=None, pair=None):
|
||||
buy_value = 1
|
||||
sell_value = 1
|
||||
return _trend(dataframe, buy_value, sell_value)
|
||||
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.populate_buy_trend = fun # Override
|
||||
backtesting.populate_sell_trend = fun # Override
|
||||
backtesting.advise_buy = fun # Override
|
||||
backtesting.advise_sell = fun # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
assert results.empty
|
||||
|
||||
|
||||
def test_backtest_only_sell(mocker, default_conf):
|
||||
# Override the default buy trend function in our default_strategy
|
||||
def fun(dataframe=None):
|
||||
def fun(dataframe=None, pair=None):
|
||||
buy_value = 0
|
||||
sell_value = 1
|
||||
return _trend(dataframe, buy_value, sell_value)
|
||||
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.populate_buy_trend = fun # Override
|
||||
backtesting.populate_sell_trend = fun # Override
|
||||
backtesting.advise_buy = fun # Override
|
||||
backtesting.advise_sell = fun # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
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)
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.populate_buy_trend = _trend_alternate # Override
|
||||
backtesting.populate_sell_trend = _trend_alternate # Override
|
||||
backtesting.advise_buy = _trend_alternate # Override
|
||||
backtesting.advise_sell = _trend_alternate # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
backtesting._store_backtest_result("test_.json", results)
|
||||
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):
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history',
|
||||
new=lambda s, n, i: _load_pair_as_ticks(n, i))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(conf)
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = MagicMock()
|
||||
|
@ -1,6 +1,5 @@
|
||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||
import os
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
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.optimize.test_backtesting import get_args
|
||||
|
||||
# Avoid to reinit the same object again and again
|
||||
_HYPEROPT_INITIALIZED = False
|
||||
_HYPEROPT = None
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def init_hyperopt(default_conf, mocker):
|
||||
global _HYPEROPT_INITIALIZED, _HYPEROPT
|
||||
if not _HYPEROPT_INITIALIZED:
|
||||
patch_exchange(mocker)
|
||||
_HYPEROPT = Hyperopt(default_conf)
|
||||
_HYPEROPT_INITIALIZED = True
|
||||
def hyperopt(default_conf, mocker):
|
||||
patch_exchange(mocker)
|
||||
return Hyperopt(default_conf)
|
||||
|
||||
|
||||
# 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*
|
||||
- we don't create any pickle'd files in the filesystem
|
||||
- we might have a pickle'd file so make sure that we return
|
||||
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.getsize', return_value=1)
|
||||
@ -45,9 +37,6 @@ def create_trials(mocker) -> None:
|
||||
|
||||
|
||||
def test_start(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test start() function
|
||||
"""
|
||||
start_mock = MagicMock()
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
@ -76,8 +65,7 @@ def test_start(mocker, default_conf, caplog) -> None:
|
||||
assert start_mock.call_count == 1
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
|
||||
hyperopt = _HYPEROPT
|
||||
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None:
|
||||
hyperopt = _HYPEROPT
|
||||
|
||||
def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None:
|
||||
shorter = hyperopt.calculate_loss(1, 100, 20)
|
||||
longer = hyperopt.calculate_loss(1, 100, 30)
|
||||
assert shorter < longer
|
||||
|
||||
|
||||
def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
|
||||
hyperopt = _HYPEROPT
|
||||
|
||||
def test_loss_calculation_has_limited_profit(hyperopt) -> None:
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
||||
hyperopt = _HYPEROPT
|
||||
def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
||||
hyperopt.current_best_loss = 2
|
||||
hyperopt.log_results(
|
||||
{
|
||||
@ -117,11 +100,10 @@ def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
||||
}
|
||||
)
|
||||
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:
|
||||
hyperopt = _HYPEROPT
|
||||
def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
|
||||
hyperopt.current_best_loss = 2
|
||||
hyperopt.log_results(
|
||||
{
|
||||
@ -131,13 +113,10 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
|
||||
assert caplog.record_tuples == []
|
||||
|
||||
|
||||
def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker)
|
||||
def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker, hyperopt)
|
||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
_HYPEROPT.trials = trials
|
||||
|
||||
hyperopt.trials = trials
|
||||
hyperopt.save_trials()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker)
|
||||
def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker, hyperopt)
|
||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
hyperopt_trial = hyperopt.read_trials()
|
||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
assert log_has(
|
||||
@ -163,7 +140,7 @@ def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
|
||||
mock_load.assert_called_once()
|
||||
|
||||
|
||||
def test_roi_table_generation(init_hyperopt) -> None:
|
||||
def test_roi_table_generation(hyperopt) -> None:
|
||||
params = {
|
||||
'roi_t1': 5,
|
||||
'roi_t2': 10,
|
||||
@ -173,11 +150,10 @@ def test_roi_table_generation(init_hyperopt) -> None:
|
||||
'roi_p3': 3,
|
||||
}
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
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())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
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)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'epochs': 1})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
default_conf.update({'config': 'config.json.example'})
|
||||
default_conf.update({'epochs': 1})
|
||||
default_conf.update({'timerange': None})
|
||||
default_conf.update({'spaces': 'all'})
|
||||
|
||||
hyperopt = Hyperopt(conf)
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||
|
||||
hyperopt.start()
|
||||
@ -203,11 +178,7 @@ def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> N
|
||||
assert dumper.called
|
||||
|
||||
|
||||
def test_format_results(init_hyperopt):
|
||||
"""
|
||||
Test Hyperopt.format_results()
|
||||
"""
|
||||
|
||||
def test_format_results(hyperopt):
|
||||
# Test with BTC as stake_currency
|
||||
trades = [
|
||||
('ETH/BTC', 2, 2, 123),
|
||||
@ -217,7 +188,7 @@ def test_format_results(init_hyperopt):
|
||||
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||
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('Total profit 1.00000000 BTC')
|
||||
assert result.find('2.0000Σ %')
|
||||
@ -229,25 +200,25 @@ def test_format_results(init_hyperopt):
|
||||
('XPR/EUR', -1, -2, -246)
|
||||
]
|
||||
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')
|
||||
|
||||
|
||||
def test_has_space(init_hyperopt):
|
||||
_HYPEROPT.config.update({'spaces': ['buy', 'roi']})
|
||||
assert _HYPEROPT.has_space('roi')
|
||||
assert _HYPEROPT.has_space('buy')
|
||||
assert not _HYPEROPT.has_space('stoploss')
|
||||
def test_has_space(hyperopt):
|
||||
hyperopt.config.update({'spaces': ['buy', 'roi']})
|
||||
assert hyperopt.has_space('roi')
|
||||
assert hyperopt.has_space('buy')
|
||||
assert not hyperopt.has_space('stoploss')
|
||||
|
||||
_HYPEROPT.config.update({'spaces': ['all']})
|
||||
assert _HYPEROPT.has_space('buy')
|
||||
hyperopt.config.update({'spaces': ['all']})
|
||||
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')
|
||||
tickerlist = {'UNITTEST/BTC': tick}
|
||||
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
|
||||
dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
|
||||
|
||||
# Check if some indicators are generated. We will not test all of them
|
||||
assert 'adx' in dataframe
|
||||
@ -255,13 +226,13 @@ def test_populate_indicators(init_hyperopt) -> None:
|
||||
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')
|
||||
tickerlist = {'UNITTEST/BTC': tick}
|
||||
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
|
||||
dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
|
||||
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,
|
||||
'fastd-value': 20,
|
||||
@ -274,17 +245,16 @@ def test_buy_strategy_generator(init_hyperopt) -> None:
|
||||
'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
|
||||
assert 'buy' in result
|
||||
assert 1 in result['buy']
|
||||
|
||||
|
||||
def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
def test_generate_optimizer(mocker, default_conf) -> None:
|
||||
default_conf.update({'config': 'config.json.example'})
|
||||
default_conf.update({'timerange': None})
|
||||
default_conf.update({'spaces': 'all'})
|
||||
|
||||
trades = [
|
||||
('POWR/BTC', 0.023117, 0.000233, 100)
|
||||
@ -324,6 +294,6 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
||||
'params': optimizer_param
|
||||
}
|
||||
|
||||
hyperopt = Hyperopt(conf)
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
|
||||
assert generate_optimizer_value == response_expected
|
||||
|
@ -11,8 +11,8 @@ from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import RPC, RPCException
|
||||
from freqtrade.state import State
|
||||
from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap,
|
||||
patch_get_signal)
|
||||
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
||||
from freqtrade.tests.conftest import patch_coinmarketcap
|
||||
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Test rpc_balance() method
|
||||
"""
|
||||
mock_balance = {
|
||||
'BTC': {
|
||||
'free': 10.0,
|
||||
|
@ -1,7 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
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:
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
default_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 == []
|
||||
|
||||
|
||||
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
||||
default_conf['telegram']['enabled'] = False
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||
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:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
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:
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.send_msg({
|
||||
'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:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
conf['webhook'] = {'enabled': False}
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
||||
default_conf['telegram']['enabled'] = False
|
||||
default_conf['webhook'] = {'enabled': False}
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
||||
assert rpc_manager.registered_modules == []
|
||||
|
@ -3,7 +3,6 @@
|
||||
# pragma pylint: disable=too-many-lines, too-many-arguments
|
||||
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from random import randint
|
||||
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.tests.conftest import (get_patched_freqtradebot, log_has,
|
||||
patch_exchange)
|
||||
from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap,
|
||||
patch_get_signal)
|
||||
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
||||
from freqtrade.tests.conftest import patch_coinmarketcap
|
||||
|
||||
|
||||
class DummyCls(Telegram):
|
||||
@ -96,9 +95,8 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
||||
update = Update(randint(1, 100))
|
||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
bot = FreqtradeBot(conf)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
bot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(bot, (True, False))
|
||||
dummy = DummyCls(bot)
|
||||
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.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
bot = FreqtradeBot(conf)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
bot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(bot, (True, False))
|
||||
dummy = DummyCls(bot)
|
||||
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.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
bot = FreqtradeBot(conf)
|
||||
bot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(bot, (True, False))
|
||||
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:
|
||||
update.message.chat.id = 123
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
conf['telegram']['chat_id'] = 123
|
||||
default_conf['telegram']['enabled'] = False
|
||||
default_conf['telegram']['chat_id'] = 123
|
||||
|
||||
patch_coinmarketcap(mocker)
|
||||
|
||||
@ -214,7 +209,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(conf)
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
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())
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] = 15.0
|
||||
freqtradebot = FreqtradeBot(conf)
|
||||
default_conf['stake_amount'] = 15.0
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
|
||||
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:
|
||||
patch_coinmarketcap(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
conf = deepcopy(default_conf)
|
||||
bot = MagicMock()
|
||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
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:
|
||||
patch_coinmarketcap(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
conf = deepcopy(default_conf)
|
||||
bot = MagicMock()
|
||||
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._config['telegram']['enabled'] = True
|
||||
|
235
freqtrade/tests/strategy/legacy_strategy.py
Normal file
235
freqtrade/tests/strategy/legacy_strategy.py
Normal 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
|
@ -25,10 +25,11 @@ def test_default_strategy_structure():
|
||||
def test_default_strategy(result):
|
||||
strategy = DefaultStrategy({})
|
||||
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
assert type(strategy.minimal_roi) is dict
|
||||
assert type(strategy.stoploss) is float
|
||||
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(strategy.populate_buy_trend(indicators)) is DataFrame
|
||||
assert type(strategy.populate_sell_trend(indicators)) is DataFrame
|
||||
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
|
||||
assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame
|
||||
|
@ -98,9 +98,6 @@ def test_get_signal_handles_exceptions(mocker, default_conf):
|
||||
|
||||
|
||||
def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||
"""
|
||||
Test Analyze.tickerdata_to_dataframe() method
|
||||
"""
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
|
||||
timerange = TimeRange(None, 'line', 0, -100)
|
||||
|
@ -1,8 +1,10 @@
|
||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||
import logging
|
||||
import os
|
||||
from os import path
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import import_strategy
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
@ -37,8 +39,8 @@ def test_import_strategy(caplog):
|
||||
|
||||
def test_search_strategy():
|
||||
default_config = {}
|
||||
default_location = os.path.join(os.path.dirname(
|
||||
os.path.realpath(__file__)), '..', '..', 'strategy'
|
||||
default_location = path.join(path.dirname(
|
||||
path.realpath(__file__)), '..', '..', 'strategy'
|
||||
)
|
||||
assert isinstance(
|
||||
StrategyResolver._search_strategy(
|
||||
@ -57,12 +59,13 @@ def test_search_strategy():
|
||||
|
||||
def test_load_strategy(result):
|
||||
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):
|
||||
resolver = StrategyResolver()
|
||||
extra_dir = os.path.join('some', 'path')
|
||||
extra_dir = path.join('some', 'path')
|
||||
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
|
||||
|
||||
assert (
|
||||
@ -70,7 +73,8 @@ def test_load_strategy_invalid_directory(result, caplog):
|
||||
logging.WARNING,
|
||||
'Path "{}" does not exist'.format(extra_dir),
|
||||
) 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():
|
||||
@ -85,7 +89,7 @@ def test_strategy(result):
|
||||
config = {'strategy': 'DefaultStrategy'}
|
||||
|
||||
resolver = StrategyResolver(config)
|
||||
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
assert resolver.strategy.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 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -150,3 +155,59 @@ def test_strategy_override_ticker_interval(caplog):
|
||||
logging.INFO,
|
||||
'Override strategy \'ticker_interval\' with value in config file: 60.'
|
||||
) 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
|
||||
|
@ -1,9 +1,5 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
"""
|
||||
Unit test file for arguments.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import pytest
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
import json
|
||||
from argparse import Namespace
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from jsonschema import ValidationError
|
||||
from jsonschema import validate, ValidationError
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.arguments import Arguments
|
||||
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:
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||
default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config(conf)
|
||||
configuration._validate_config(default_conf)
|
||||
|
||||
|
||||
def test_load_config_missing_attributes(default_conf) -> None:
|
||||
conf = deepcopy(default_conf)
|
||||
conf.pop('exchange')
|
||||
default_conf.pop('exchange')
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config(conf)
|
||||
configuration._validate_config(default_conf)
|
||||
|
||||
|
||||
def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] = 'fake'
|
||||
default_conf['stake_amount'] = 'fake'
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config(conf)
|
||||
configuration._validate_config(default_conf)
|
||||
|
||||
|
||||
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:
|
||||
conf = deepcopy(default_conf)
|
||||
conf['max_open_trades'] = 0
|
||||
default_conf['max_open_trades'] = 0
|
||||
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')
|
||||
@ -151,13 +147,12 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
|
||||
|
||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||
custom_conf = deepcopy(default_conf)
|
||||
custom_conf.update({
|
||||
default_conf.update({
|
||||
'strategy': 'CustomStrategy',
|
||||
'strategy_path': '/tmp/strategies',
|
||||
})
|
||||
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()
|
||||
@ -322,29 +317,25 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_check_exchange(default_conf) -> None:
|
||||
"""
|
||||
Test the configuration validator with a missing attribute
|
||||
"""
|
||||
conf = deepcopy(default_conf)
|
||||
configuration = Configuration(Namespace())
|
||||
|
||||
# Test a valid exchange
|
||||
conf.get('exchange').update({'name': 'BITTREX'})
|
||||
assert configuration.check_exchange(conf)
|
||||
default_conf.get('exchange').update({'name': 'BITTREX'})
|
||||
assert configuration.check_exchange(default_conf)
|
||||
|
||||
# Test a valid exchange
|
||||
conf.get('exchange').update({'name': 'binance'})
|
||||
assert configuration.check_exchange(conf)
|
||||
default_conf.get('exchange').update({'name': 'binance'})
|
||||
assert configuration.check_exchange(default_conf)
|
||||
|
||||
# Test a invalid exchange
|
||||
conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||
configuration.config = conf
|
||||
default_conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||
configuration.config = default_conf
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
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:
|
||||
@ -398,3 +389,7 @@ def test_set_loggers() -> None:
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
|
||||
|
||||
def test_validate_default_conf(default_conf) -> None:
|
||||
validate(default_conf, constants.CONF_SCHEMA)
|
||||
|
@ -14,7 +14,7 @@ def load_dataframe_pair(pairs, strategy):
|
||||
assert isinstance(pairs[0], str)
|
||||
dataframe = ld[pairs[0]]
|
||||
|
||||
dataframe = strategy.analyze_ticker(dataframe)
|
||||
dataframe = strategy.analyze_ticker(dataframe, pairs[0])
|
||||
return dataframe
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@ -23,46 +22,40 @@ def test_init_create_session(default_conf):
|
||||
|
||||
|
||||
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
|
||||
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())
|
||||
|
||||
init(conf)
|
||||
init(default_conf)
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
||||
|
||||
|
||||
def test_init_invalid_db_url(default_conf):
|
||||
conf = deepcopy(default_conf)
|
||||
|
||||
# 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*'):
|
||||
init(conf)
|
||||
init(default_conf)
|
||||
|
||||
|
||||
def test_init_prod_db(default_conf, mocker):
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'dry_run': False})
|
||||
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||
default_conf.update({'dry_run': False})
|
||||
default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||
|
||||
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.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||
|
||||
|
||||
def test_init_dryrun_db(default_conf, mocker):
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'dry_run': True})
|
||||
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
||||
default_conf.update({'dry_run': True})
|
||||
default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
||||
|
||||
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.mock_calls[0][1][0] == 'sqlite://'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
ccxt==1.17.45
|
||||
ccxt==1.17.49
|
||||
SQLAlchemy==1.2.10
|
||||
python-telegram-bot==10.1.0
|
||||
arrow==0.12.1
|
||||
|
@ -159,8 +159,8 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||
dataframes = strategy.tickerdata_to_dataframe(tickers)
|
||||
|
||||
dataframe = dataframes[pair]
|
||||
dataframe = strategy.populate_buy_trend(dataframe)
|
||||
dataframe = strategy.populate_sell_trend(dataframe)
|
||||
dataframe = strategy.advise_buy(dataframe, {'pair': pair})
|
||||
dataframe = strategy.advise_sell(dataframe, {'pair': pair})
|
||||
|
||||
if len(dataframe.index) > args.plot_limit:
|
||||
logger.warning('Ticker contained more than %s candles as defined '
|
||||
|
2
setup.py
2
setup.py
@ -18,7 +18,7 @@ setup(name='freqtrade',
|
||||
license='GPLv3',
|
||||
packages=['freqtrade'],
|
||||
scripts=['bin/freqtrade'],
|
||||
setup_requires=['pytest-runner'],
|
||||
setup_requires=['pytest-runner', 'numpy'],
|
||||
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
||||
install_requires=[
|
||||
'ccxt',
|
||||
|
@ -18,6 +18,7 @@ class TestStrategy(IStrategy):
|
||||
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
|
||||
|
||||
You can:
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
- Rename the class name (Do not forget to update class_name)
|
||||
- Add any methods you want 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
|
||||
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
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
@ -211,10 +215,11 @@ class TestStrategy(IStrategy):
|
||||
|
||||
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
|
||||
:param dataframe: DataFrame
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
@ -227,10 +232,11 @@ class TestStrategy(IStrategy):
|
||||
|
||||
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
|
||||
:param dataframe: DataFrame
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
|
Loading…
Reference in New Issue
Block a user