Merge branch 'develop' into feature-unlimited-stake_amount

This commit is contained in:
Anton 2018-06-17 02:23:40 +03:00
commit ae94ab17f4
31 changed files with 652 additions and 907 deletions

View File

@ -6,10 +6,12 @@ If it hasn't been reported, please create a new issue.
## Step 2: Describe your environment ## Step 2: Describe your environment
* Python Version: _____ (`python -V`) * Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`)
* Branch: Master | Develop * Branch: Master | Develop
* Last Commit ID: _____ (`git log --format="%H" -n 1`) * Last Commit ID: _____ (`git log --format="%H" -n 1`)
## Step 3: Describe the problem: ## Step 3: Describe the problem:
*Explain the problem you have encountered* *Explain the problem you have encountered*
### Steps to reproduce: ### Steps to reproduce:

View File

@ -16,16 +16,15 @@ install:
- pip install --upgrade flake8 coveralls pytest-random-order mypy - pip install --upgrade flake8 coveralls pytest-random-order mypy
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install -e . - pip install -e .
- cp config.json.example config.json
jobs: jobs:
include: include:
- script: - script:
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
- coveralls - coveralls
- script: - script:
- cp config.json.example config.json
- python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting - python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting
- script: - script:
- cp config.json.example config.json
- python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5 - python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5
- script: flake8 freqtrade - script: flake8 freqtrade
- script: mypy freqtrade - script: mypy freqtrade

View File

@ -1,17 +1,19 @@
# Backtesting # Backtesting
This page explains how to validate your strategy performance by using This page explains how to validate your strategy performance by using
Backtesting. Backtesting.
## Table of Contents ## Table of Contents
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting) - [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
- [Understand the backtesting result](#understand-the-backtesting-result) - [Understand the backtesting result](#understand-the-backtesting-result)
## Test your strategy with Backtesting ## Test your strategy with Backtesting
Now you have good Buy and Sell strategies, you want to test it against Now you have good Buy and Sell strategies, you want to test it against
real data. This is what we call real data. This is what we call
[backtesting](https://en.wikipedia.org/wiki/Backtesting). [backtesting](https://en.wikipedia.org/wiki/Backtesting).
Backtesting will use the crypto-currencies (pair) from your config file Backtesting will use the crypto-currencies (pair) from your config file
and load static tickers located in and load static tickers located in
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata). [/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
@ -19,70 +21,80 @@ If the 5 min and 1 min ticker for the crypto-currencies to test is not
already in the `testdata` folder, backtesting will download them already in the `testdata` folder, backtesting will download them
automatically. Testdata files will not be updated until you specify it. automatically. Testdata files will not be updated until you specify it.
The result of backtesting will confirm you if your bot as more chance to The result of backtesting will confirm you if your bot has better odds of making a profit than a loss.
make a profit than a loss.
The backtesting is very easy with freqtrade. The backtesting is very easy with freqtrade.
### Run a backtesting against the currencies listed in your config file ### Run a backtesting against the currencies listed in your config file
**With 5 min tickers (Per default)** #### With 5 min tickers (Per default)
```bash ```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation python3 ./freqtrade/main.py backtesting --realistic-simulation
``` ```
**With 1 min tickers** #### With 1 min tickers
```bash ```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
``` ```
**Update cached pairs with the latest data** #### Update cached pairs with the latest data
```bash ```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
``` ```
**With live data (do not alter your testdata files)** #### With live data (do not alter your testdata files)
```bash ```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --live python3 ./freqtrade/main.py backtesting --realistic-simulation --live
``` ```
**Using a different on-disk ticker-data source** #### Using a different on-disk ticker-data source
```bash ```bash
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101 python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
``` ```
**With a (custom) strategy file** #### With a (custom) strategy file
```bash ```bash
python3 ./freqtrade/main.py -s TestStrategy backtesting python3 ./freqtrade/main.py -s TestStrategy backtesting
``` ```
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
**Exporting trades to file** #### Exporting trades to file
```bash ```bash
python3 ./freqtrade/main.py backtesting --export trades python3 ./freqtrade/main.py backtesting --export trades
``` ```
**Exporting trades to file specifying a custom filename** #### Exporting trades to file specifying a custom filename
```bash ```bash
python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json
``` ```
#### Running backtest with smaller testset
**Running backtest with smaller testset**
Use the `--timerange` argument to change how much of the testset Use the `--timerange` argument to change how much of the testset
you want to use. The last N ticks/timeframes will be used. you want to use. The last N ticks/timeframes will be used.
Example: Example:
```bash ```bash
python3 ./freqtrade/main.py backtesting --timerange=-200 python3 ./freqtrade/main.py backtesting --timerange=-200
``` ```
***Advanced use of timerange*** #### Advanced use of timerange
Doing `--timerange=-200` will get the last 200 timeframes Doing `--timerange=-200` will get the last 200 timeframes
from your inputdata. You can also specify specific dates, from your inputdata. You can also specify specific dates,
or a range span indexed by start and stop. or a range span indexed by start and stop.
The full timerange specification: The full timerange specification:
- Use last 123 tickframes of data: `--timerange=-123` - Use last 123 tickframes of data: `--timerange=-123`
- Use first 123 tickframes of data: `--timerange=123-` - Use first 123 tickframes of data: `--timerange=123-`
- Use tickframes from line 123 through 456: `--timerange=123-456` - Use tickframes from line 123 through 456: `--timerange=123-456`
@ -92,11 +104,12 @@ The full timerange specification:
- Use tickframes between POSIX timestamps 1527595200 1527618600: - Use tickframes between POSIX timestamps 1527595200 1527618600:
`--timerange=1527595200-1527618600` `--timerange=1527595200-1527618600`
#### Downloading new set of ticker data
**Downloading new set of ticker data**
To download new set of backtesting ticker data, you can use a download script. To download new set of backtesting ticker data, you can use a download script.
If you are using Binance for example: If you are using Binance for example:
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder. - create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
- update the `pairs.json` to contain the currency pairs you are interested in. - update the `pairs.json` to contain the currency pairs you are interested in.
@ -119,33 +132,55 @@ This will download ticker data for all the currency pairs you defined in `pairs.
- To download ticker data for only 10 days, use `--days 10`. - To download ticker data for only 10 days, use `--days 10`.
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. - Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
For help about backtesting usage, please refer to
[Backtesting commands](#backtesting-commands).
## Understand the backtesting result ## Understand the backtesting result
The most important in the backtesting is to understand the result. The most important in the backtesting is to understand the result.
A backtesting result will look like that: A backtesting result will look like that:
``` ```
====================== BACKTESTING REPORT ================================ ======================================== BACKTESTING REPORT =========================================
pair buy count avg profit % total profit BTC avg duration | pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
-------- ----------- -------------- ------------------ -------------- |:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
ETH/BTC 56 -0.67 -0.00075455 62.3 | ETH/BTC | 44 | 0.18 | 0.00159118 | 50.9 | 44 | 0 |
LTC/BTC 38 -0.48 -0.00036315 57.9 | LTC/BTC | 27 | 0.10 | 0.00051931 | 103.1 | 26 | 1 |
ETC/BTC 42 -1.15 -0.00096469 67.0 | ETC/BTC | 24 | 0.05 | 0.00022434 | 166.0 | 22 | 2 |
DASH/BTC 72 -0.62 -0.00089368 39.9 | DASH/BTC | 29 | 0.18 | 0.00103223 | 192.2 | 29 | 0 |
ZEC/BTC 45 -0.46 -0.00041387 63.2 | ZEC/BTC | 65 | -0.02 | -0.00020621 | 202.7 | 62 | 3 |
XLM/BTC 24 -0.88 -0.00041846 47.7 | XLM/BTC | 35 | 0.02 | 0.00012877 | 242.4 | 32 | 3 |
NXT/BTC 24 0.68 0.00031833 40.2 | BCH/BTC | 12 | 0.62 | 0.00149284 | 50.0 | 12 | 0 |
POWR/BTC 35 0.98 0.00064887 45.3 | POWR/BTC | 21 | 0.26 | 0.00108215 | 134.8 | 21 | 0 |
ADA/BTC 43 -0.39 -0.00032292 55.0 | ADA/BTC | 54 | -0.19 | -0.00205202 | 191.3 | 47 | 7 |
XMR/BTC 40 -0.40 -0.00032181 47.4 | XMR/BTC | 24 | -0.43 | -0.00206013 | 120.6 | 20 | 4 |
TOTAL 419 -0.41 -0.00348593 52.9 | TOTAL | 335 | 0.03 | 0.00175246 | 157.9 | 315 | 20 |
2018-06-13 06:57:27,347 - freqtrade.optimize.backtesting - INFO -
====================================== LEFT OPEN TRADES REPORT ======================================
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
| ETH/BTC | 3 | 0.16 | 0.00009619 | 25.0 | 3 | 0 |
| LTC/BTC | 1 | -1.00 | -0.00020118 | 1085.0 | 0 | 1 |
| ETC/BTC | 2 | -1.80 | -0.00071933 | 1092.5 | 0 | 2 |
| DASH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
| ZEC/BTC | 3 | -4.27 | -0.00256826 | 1301.7 | 0 | 3 |
| XLM/BTC | 3 | -1.11 | -0.00066744 | 965.0 | 0 | 3 |
| BCH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
| POWR/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
| ADA/BTC | 7 | -3.58 | -0.00503604 | 850.0 | 0 | 7 |
| XMR/BTC | 4 | -3.79 | -0.00303456 | 291.2 | 0 | 4 |
| TOTAL | 23 | -2.63 | -0.01213062 | 750.4 | 3 | 20 |
``` ```
The 1st table will contain all trades the bot made.
The 2nd table will contain all trades the bot had to `forcesell` at the end of the backtest period to prsent a full picture.
These trades are also included in the first table, but are extracted separately for clarity.
The last line will give you the overall performance of your strategy, The last line will give you the overall performance of your strategy,
here: here:
``` ```
TOTAL 419 -0.41 -0.00348593 52.9 TOTAL 419 -0.41 -0.00348593 52.9
``` ```
@ -161,6 +196,7 @@ strategy, your sell strategy, and also by the `minimal_roi` and
As for an example if your minimal_roi is only `"0": 0.01`. You cannot As for an example if your minimal_roi is only `"0": 0.01`. You cannot
expect the bot to make more profit than 1% (because it will sell every expect the bot to make more profit than 1% (because it will sell every
time a trade will reach 1%). time a trade will reach 1%).
```json ```json
"minimal_roi": { "minimal_roi": {
"0": 0.01 "0": 0.01
@ -173,6 +209,7 @@ profit. Hence, keep in mind that your performance is a mix of your
strategies, your configuration, and the crypto-currency you have set up. strategies, your configuration, and the crypto-currency you have set up.
## Next step ## Next step
Great, your strategy is profitable. What if the bot can give your the Great, your strategy is profitable. What if the bot can give your the
optimal parameters to use for your strategy? optimal parameters to use for your strategy?
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)

View File

@ -160,13 +160,12 @@ the parameter `-l` or `--live`.
## Hyperopt commands ## Hyperopt commands
It is possible to use hyperopt for trading strategy optimization. To optimize your strategy, you can use hyperopt parameter hyperoptimization
Hyperopt uses an internal json config return by `hyperopt_optimize_conf()` to find optimal parameter values for your stategy.
located in `freqtrade/optimize/hyperopt_conf.py`.
``` ```
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation] usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
[--timerange TIMERANGE] [-e INT] [--use-mongodb] [--timerange TIMERANGE] [-e INT]
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]] [-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
optional arguments: optional arguments:
@ -176,11 +175,8 @@ optional arguments:
--realistic-simulation --realistic-simulation
uses max_open_trades from config to simulate real uses max_open_trades from config to simulate real
world limitations world limitations
--timerange TIMERANGE --timerange TIMERANGE specify what timerange of data to use.
specify what timerange of data to use.
-e INT, --epochs INT specify number of epochs (default: 100) -e INT, --epochs INT specify number of epochs (default: 100)
--use-mongodb parallelize evaluations with mongodb (requires mongod
in PATH)
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...] -s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
Specify which parameters to hyperopt. Space separate Specify which parameters to hyperopt. Space separate
list. Default: all list. Default: all

View File

@ -9,7 +9,6 @@ parameters with Hyperopt.
- [Advanced Hyperopt notions](#advanced-notions) - [Advanced Hyperopt notions](#advanced-notions)
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers) - [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
- [Execute Hyperopt](#execute-hyperopt) - [Execute Hyperopt](#execute-hyperopt)
- [Hyperopt with MongoDB](#hyperopt-with-mongoDB)
- [Understand the hyperopts result](#understand-the-backtesting-result) - [Understand the hyperopts result](#understand-the-backtesting-result)
## Prepare Hyperopt ## Prepare Hyperopt
@ -194,41 +193,6 @@ Legal values are:
- `stoploss`: search for the best stoploss value - `stoploss`: search for the best stoploss value
- space-separated list of any of the above values for example `--spaces roi stoploss` - space-separated list of any of the above values for example `--spaces roi stoploss`
### Hyperopt with MongoDB
Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by
executing the previous command is the execution takes a long time.
To accelerate it you can use hyperopt with MongoDB.
To run hyperopt with MongoDb you will need 3 terminals.
**Terminal 1: Start MongoDB**
```bash
cd <freqtrade>
source .env/bin/activate
python3 scripts/start-mongodb.py
```
**Terminal 2: Start Hyperopt worker**
```bash
cd <freqtrade>
source .env/bin/activate
python3 scripts/start-hyperopt-worker.py
```
**Terminal 3: Start Hyperopt with MongoDB**
```bash
cd <freqtrade>
source .env/bin/activate
python3 ./freqtrade/main.py -c config.json hyperopt --use-mongodb
```
**Re-run an Hyperopt**
To re-run Hyperopt you have to delete the existing MongoDB table.
```bash
cd <freqtrade>
rm -rf .hyperopt/mongodb/
```
## Understand the hyperopts result ## Understand the hyperopts result
Once Hyperopt is completed you can use the result to adding new buy Once Hyperopt is completed you can use the result to adding new buy
signal. Given following result from hyperopt: signal. Given following result from hyperopt:

View File

@ -225,17 +225,7 @@ cd ..
rm -rf ./ta-lib* rm -rf ./ta-lib*
``` ```
#### 3. [Optional] Install MongoDB #### 3. Install FreqTrade
Install MongoDB if you plan to optimize your strategy with Hyperopt.
```bash
sudo apt-get install mongodb-org
```
> Complete tutorial from Digital Ocean: [How to Install MongoDB on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-16-04).
#### 4. Install FreqTrade
Clone the git repository: Clone the git repository:
@ -243,7 +233,7 @@ Clone the git repository:
git clone https://github.com/freqtrade/freqtrade.git git clone https://github.com/freqtrade/freqtrade.git
``` ```
#### 5. Configure `freqtrade` as a `systemd` service #### 4. Configure `freqtrade` as a `systemd` service
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup. From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
@ -267,19 +257,7 @@ sudo loginctl enable-linger "$USER"
brew install python3 git wget ta-lib brew install python3 git wget ta-lib
``` ```
#### 2. [Optional] Install MongoDB #### 2. Install FreqTrade
Install MongoDB if you plan to optimize your strategy with Hyperopt.
```bash
curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl-x86_64-3.4.10.tgz
tar -zxvf mongodb-osx-ssl-x86_64-3.4.10.tgz
mkdir -p <path_freqtrade>/env/mongodb
cp -R -n mongodb-osx-x86_64-3.4.10/ <path_freqtrade>/env/mongodb
export PATH=<path_freqtrade>/env/mongodb/bin:$PATH
```
#### 3. Install FreqTrade
Clone the git repository: Clone the git repository:

View File

@ -203,12 +203,6 @@ class Arguments(object):
type=int, type=int,
metavar='INT', metavar='INT',
) )
parser.add_argument(
'--use-mongodb',
help='parallelize evaluations with mongodb (requires mongod in PATH)',
dest='mongodb',
action='store_true',
)
parser.add_argument( parser.add_argument(
'-s', '--spaces', '-s', '--spaces',
help='Specify which parameters to hyperopt. Space separate list. \ help='Specify which parameters to hyperopt. Space separate list. \

View File

@ -188,11 +188,6 @@ class Configuration(object):
logger.info('Parameter --epochs detected ...') logger.info('Parameter --epochs detected ...')
logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs')) logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
# If --mongodb is used we add it to the configuration
if 'mongodb' in self.args and self.args.mongodb:
config.update({'mongodb': self.args.mongodb})
logger.info('Parameter --use-mongodb detected ...')
# If --spaces is used we add it to the configuration # If --spaces is used we add it to the configuration
if 'spaces' in self.args and self.args.spaces: if 'spaces' in self.args and self.args.spaces:
config.update({'spaces': self.args.spaces}) config.update({'spaces': self.args.spaces})

View File

@ -11,8 +11,6 @@ from freqtrade import misc, constants
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import get_ticker_history
from freqtrade.arguments import TimeRange from freqtrade.arguments import TimeRange
from user_data.hyperopt_conf import hyperopt_optimize_conf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -83,7 +81,7 @@ def load_tickerdata_file(
def load_data(datadir: str, def load_data(datadir: str,
ticker_interval: str, ticker_interval: str,
pairs: Optional[List[str]] = None, pairs: List[str],
refresh_pairs: Optional[bool] = False, refresh_pairs: Optional[bool] = False,
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]: timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
""" """
@ -92,14 +90,12 @@ def load_data(datadir: str,
""" """
result = {} result = {}
_pairs = pairs or hyperopt_optimize_conf()['exchange']['pair_whitelist']
# If the user force the refresh of pairs # If the user force the refresh of pairs
if refresh_pairs: if refresh_pairs:
logger.info('Download data for all pairs and store them in %s', datadir) logger.info('Download data for all pairs and store them in %s', datadir)
download_pairs(datadir, _pairs, ticker_interval, timerange=timerange) download_pairs(datadir, pairs, ticker_interval, timerange=timerange)
for pair in _pairs: for pair in pairs:
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
if pairdata: if pairdata:
result[pair] = pairdata result[pair] = pairdata

View File

@ -6,7 +6,8 @@ This module contains the backtesting logic
import logging import logging
import operator import operator
from argparse import Namespace from argparse import Namespace
from typing import Dict, Tuple, Any, List, Optional from datetime import datetime
from typing import Dict, Tuple, Any, List, Optional, NamedTuple
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
@ -23,6 +24,21 @@ from freqtrade.persistence import Trade
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BacktestResult(NamedTuple):
"""
NamedTuple Defining BacktestResults inputs.
"""
pair: str
profit_percent: float
profit_abs: float
open_time: datetime
close_time: datetime
open_index: int
close_index: int
trade_duration: float
open_at_end: bool
class Backtesting(object): class Backtesting(object):
""" """
Backtesting class, this class contains all the logic to run a backtest Backtesting class, this class contains all the logic to run a backtest
@ -73,15 +89,15 @@ class Backtesting(object):
headers = ['pair', 'buy count', 'avg profit %', headers = ['pair', 'buy count', 'avg profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] 'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data: for pair in data:
result = results[results.currency == pair] result = results[results.pair == pair]
tabular_data.append([ tabular_data.append([
pair, pair,
len(result.index), len(result.index),
result.profit_percent.mean() * 100.0, result.profit_percent.mean() * 100.0,
result.profit_BTC.sum(), result.profit_abs.sum(),
result.duration.mean(), result.trade_duration.mean(),
len(result[result.profit_BTC > 0]), len(result[result.profit_abs > 0]),
len(result[result.profit_BTC < 0]) len(result[result.profit_abs < 0])
]) ])
# Append Total # Append Total
@ -89,16 +105,28 @@ class Backtesting(object):
'TOTAL', 'TOTAL',
len(results.index), len(results.index),
results.profit_percent.mean() * 100.0, results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(), results.profit_abs.sum(),
results.duration.mean(), results.trade_duration.mean(),
len(results[results.profit_BTC > 0]), len(results[results.profit_abs > 0]),
len(results[results.profit_BTC < 0]) len(results[results.profit_abs < 0])
]) ])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
records = [(trade_entry.pair, trade_entry.profit_percent,
trade_entry.open_time.timestamp(),
trade_entry.close_time.timestamp(),
trade_entry.open_index - 1, trade_entry.trade_duration)
for index, trade_entry in results.iterrows()]
if records:
logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records)
def _get_sell_trade_entry( def _get_sell_trade_entry(
self, pair: str, buy_row: DataFrame, self, pair: str, buy_row: DataFrame,
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[Tuple]: partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[BacktestResult]:
stake_amount = args['stake_amount'] stake_amount = args['stake_amount']
max_open_trades = args.get('max_open_trades', 0) max_open_trades = args.get('max_open_trades', 0)
@ -121,15 +149,33 @@ class Backtesting(object):
buy_signal = sell_row.buy buy_signal = sell_row.buy
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal, if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
sell_row.sell): sell_row.sell):
return \
sell_row, \ return BacktestResult(pair=pair,
( profit_percent=trade.calc_profit_percent(rate=sell_row.close),
pair, profit_abs=trade.calc_profit(rate=sell_row.close),
trade.calc_profit_percent(rate=sell_row.close), open_time=buy_row.date,
trade.calc_profit(rate=sell_row.close), close_time=sell_row.date,
(sell_row.date - buy_row.date).seconds // 60 trade_duration=(sell_row.date - buy_row.date).seconds // 60,
), \ open_index=buy_row.Index,
sell_row.date close_index=sell_row.Index,
open_at_end=False
)
if partial_ticker:
# no sell condition found - trade stil open at end of backtest period
sell_row = partial_ticker[-1]
btr = BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
profit_abs=trade.calc_profit(rate=sell_row.close),
open_time=buy_row.date,
close_time=sell_row.date,
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index,
close_index=sell_row.Index,
open_at_end=True
)
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
btr.profit_percent, btr.profit_abs)
return btr
return None return None
def backtest(self, args: Dict) -> DataFrame: def backtest(self, args: Dict) -> DataFrame:
@ -145,17 +191,12 @@ class Backtesting(object):
processed: a processed dictionary with format {pair, data} processed: a processed dictionary with format {pair, data}
max_open_trades: maximum number of concurrent trades (default: 0, disabled) max_open_trades: maximum number of concurrent trades (default: 0, disabled)
realistic: do we try to simulate realistic trades? (default: True) realistic: do we try to simulate realistic trades? (default: True)
sell_profit_only: sell if profit only
use_sell_signal: act on sell-signal
:return: DataFrame :return: DataFrame
""" """
headers = ['date', 'buy', 'open', 'close', 'sell'] headers = ['date', 'buy', 'open', 'close', 'sell']
processed = args['processed'] processed = args['processed']
max_open_trades = args.get('max_open_trades', 0) max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', False) realistic = args.get('realistic', False)
record = args.get('record', None)
recordfilename = args.get('recordfn', 'backtest-result.json')
records = []
trades = [] trades = []
trade_count_lock: Dict = {} trade_count_lock: Dict = {}
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
@ -170,6 +211,8 @@ class Backtesting(object):
ticker_data.drop(ticker_data.head(1).index, inplace=True) ticker_data.drop(ticker_data.head(1).index, inplace=True)
# Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.)
ticker = [x for x in ticker_data.itertuples()] ticker = [x for x in ticker_data.itertuples()]
lock_pair_until = None lock_pair_until = None
@ -187,28 +230,18 @@ class Backtesting(object):
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
ret = self._get_sell_trade_entry(pair, row, ticker[index + 1:], trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
trade_count_lock, args) trade_count_lock, args)
if ret: if trade_entry:
row2, trade_entry, next_date = ret lock_pair_until = trade_entry.close_time
lock_pair_until = next_date
trades.append(trade_entry) trades.append(trade_entry)
if record: else:
# Note, need to be json.dump friendly # Set lock_pair_until to end of testing period if trade could not be closed
# record a tuple of pair, current_profit_percent, # This happens only if the buy-signal was with the last candle
# entry-date, duration lock_pair_until = ticker_data.iloc[-1].date
records.append((pair, trade_entry[1],
row.date.strftime('%s'), return DataFrame.from_records(trades, columns=BacktestResult._fields)
row2.date.strftime('%s'),
index, trade_entry[3]))
# For now export inside backtest(), maybe change so that backtest()
# returns a tuple like: (dataframe, records, logs, etc)
if record and record.find('trades') >= 0:
logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records)
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
return DataFrame.from_records(trades, columns=labels)
def start(self) -> None: def start(self) -> None:
""" """
@ -237,6 +270,9 @@ class Backtesting(object):
timerange=timerange timerange=timerange
) )
if not data:
logger.critical("No data found. Terminating.")
return
# Ignore max_open_trades in backtesting, except realistic flag was passed # Ignore max_open_trades in backtesting, except realistic flag was passed
if self.config.get('realistic_simulation', False): if self.config.get('realistic_simulation', False):
max_open_trades = self.config['max_open_trades'] max_open_trades = self.config['max_open_trades']
@ -256,24 +292,22 @@ class Backtesting(object):
) )
# Execute backtest and print results # Execute backtest and print results
sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False)
use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False)
results = self.backtest( results = self.backtest(
{ {
'stake_amount': self.config.get('stake_amount'), 'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed, 'processed': preprocessed,
'max_open_trades': max_open_trades, 'max_open_trades': max_open_trades,
'realistic': self.config.get('realistic_simulation', False), 'realistic': self.config.get('realistic_simulation', False),
'sell_profit_only': sell_profit_only,
'use_sell_signal': use_sell_signal,
'record': self.config.get('export'),
'recordfn': self.config.get('exportfilename'),
} }
) )
if self.config.get('export', False):
self._store_backtest_result(self.config.get('exportfilename'), results)
logger.info( logger.info(
'\n==================================== ' '\n======================================== '
'BACKTESTING REPORT' 'BACKTESTING REPORT'
' ====================================\n' ' =========================================\n'
'%s', '%s',
self._generate_text_table( self._generate_text_table(
data, data,
@ -281,6 +315,17 @@ class Backtesting(object):
) )
) )
logger.info(
'\n====================================== '
'LEFT OPEN TRADES REPORT'
' ======================================\n'
'%s',
self._generate_text_table(
data,
results.loc[results.open_at_end]
)
)
def setup_configuration(args: Namespace) -> Dict[str, Any]: def setup_configuration(args: Namespace) -> Dict[str, Any]:
""" """

View File

@ -19,7 +19,6 @@ from typing import Dict, Any, Callable, Optional
import numpy import numpy
import talib.abstract as ta import talib.abstract as ta
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
from hyperopt.mongoexp import MongoTrials
from pandas import DataFrame from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
@ -27,7 +26,6 @@ from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.optimize import load_data from freqtrade.optimize import load_data
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from user_data.hyperopt_conf import hyperopt_optimize_conf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -451,7 +449,7 @@ class Hyperopt(Backtesting):
total_profit = results.profit_percent.sum() total_profit = results.profit_percent.sum()
trade_count = len(results.index) trade_count = len(results.index)
trade_duration = results.duration.mean() trade_duration = results.trade_duration.mean()
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration: if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
print('.', end='') print('.', end='')
@ -488,10 +486,10 @@ class Hyperopt(Backtesting):
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format( 'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
len(results.index), len(results.index),
results.profit_percent.mean() * 100.0, results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(), results.profit_abs.sum(),
self.config['stake_currency'], self.config['stake_currency'],
results.profit_percent.sum(), results.profit_percent.sum(),
results.duration.mean(), results.trade_duration.mean(),
) )
def start(self) -> None: def start(self) -> None:
@ -508,32 +506,20 @@ class Hyperopt(Backtesting):
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
self.processed = self.tickerdata_to_dataframe(data) self.processed = self.tickerdata_to_dataframe(data)
if self.config.get('mongodb'): logger.info('Preparing Trials..')
logger.info('Using mongodb ...') signal.signal(signal.SIGINT, self.signal_handler)
# read trials file if we have one
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
self.trials = self.read_trials()
self.current_tries = len(self.trials.results)
self.total_tries += self.current_tries
logger.info( logger.info(
'Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!' 'Continuing with trials. Current: %d, Total: %d',
self.current_tries,
self.total_tries
) )
db_name = 'freqtrade_hyperopt'
self.trials = MongoTrials(
arg='mongo://127.0.0.1:1234/{}/jobs'.format(db_name),
exp_key='exp1'
)
else:
logger.info('Preparing Trials..')
signal.signal(signal.SIGINT, self.signal_handler)
# read trials file if we have one
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
self.trials = self.read_trials()
self.current_tries = len(self.trials.results)
self.total_tries += self.current_tries
logger.info(
'Continuing with trials. Current: %d, Total: %d',
self.current_tries,
self.total_tries
)
try: try:
best_parameters = fmin( best_parameters = fmin(
fn=self.generate_optimizer, fn=self.generate_optimizer,
@ -589,18 +575,14 @@ def start(args: Namespace) -> None:
""" """
# Remove noisy log messages # Remove noisy log messages
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
# Initialize configuration # Initialize configuration
# Monkey patch the configuration with hyperopt_conf.py # Monkey patch the configuration with hyperopt_conf.py
configuration = Configuration(args) configuration = Configuration(args)
logger.info('Starting freqtrade in Hyperopt mode') logger.info('Starting freqtrade in Hyperopt mode')
config = configuration.load_config()
optimize_config = hyperopt_optimize_conf()
config = configuration._load_common_config(optimize_config)
config = configuration._load_backtesting_config(config)
config = configuration._load_hyperopt_config(config)
config['exchange']['key'] = '' config['exchange']['key'] = ''
config['exchange']['secret'] = '' config['exchange']['secret'] = ''

View File

@ -2,24 +2,34 @@
This module contains class to define a RPC communications This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
from decimal import Decimal from decimal import Decimal
from typing import Dict, Tuple, Any from typing import Dict, Tuple, Any, List
import arrow import arrow
import sqlalchemy as sql import sqlalchemy as sql
from pandas import DataFrame
from numpy import mean, nan_to_num from numpy import mean, nan_to_num
from pandas import DataFrame
from freqtrade import exchange from freqtrade import exchange
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.state import State from freqtrade.state import State
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RPCException(Exception):
"""
Should be raised with a rpc-formatted message in an _rpc_* method
if the required state is wrong, i.e.:
raise RPCException('*Status:* `no active trade`')
"""
pass
class RPC(object): class RPC(object):
""" """
RPC class can be used to have extra feature, like bot data, and access to DB data RPC class can be used to have extra feature, like bot data, and access to DB data
@ -30,20 +40,32 @@ class RPC(object):
:param freqtrade: Instance of a freqtrade bot :param freqtrade: Instance of a freqtrade bot
:return: None :return: None
""" """
self.freqtrade = freqtrade self._freqtrade = freqtrade
def rpc_trade_status(self) -> Tuple[bool, Any]: @abstractmethod
def cleanup(self) -> None:
""" Cleanup pending module resources """
@property
@abstractmethod
def name(self) -> str:
""" Returns the lowercase name of this module """
@abstractmethod
def send_msg(self, msg: str) -> None:
""" Sends a message to all registered rpc modules """
def _rpc_trade_status(self) -> List[str]:
""" """
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function a remotely exposed function
:return:
""" """
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '*Status:* `trader is not running`' raise RPCException('*Status:* `trader is not running`')
elif not trades: elif not trades:
return True, '*Status:* `no active trade`' raise RPCException('*Status:* `no active trade`')
else: else:
result = [] result = []
for trade in trades: for trade in trades:
@ -82,14 +104,14 @@ class RPC(object):
) if order else None, ) if order else None,
) )
result.append(message) result.append(message)
return False, result return result
def rpc_status_table(self) -> Tuple[bool, Any]: def _rpc_status_table(self) -> DataFrame:
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '*Status:* `trader is not running`' raise RPCException('*Status:* `trader is not running`')
elif not trades: elif not trades:
return True, '*Status:* `no active order`' raise RPCException('*Status:* `no active order`')
else: else:
trades_list = [] trades_list = []
for trade in trades: for trade in trades:
@ -105,22 +127,18 @@ class RPC(object):
columns = ['ID', 'Pair', 'Since', 'Profit'] columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns) df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0]) df_statuses = df_statuses.set_index(columns[0])
# The style used throughout is to return a tuple return df_statuses
# consisting of (error_occured?, result)
# Another approach would be to just return the
# result, or raise error
return False, df_statuses
def rpc_daily_profit( def _rpc_daily_profit(
self, timescale: int, self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]: stake_currency: str, fiat_display_currency: str) -> List[List[Any]]:
today = datetime.utcnow().date() today = datetime.utcnow().date()
profit_days: Dict[date, Dict] = {} profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0): if not (isinstance(timescale, int) and timescale > 0):
return True, '*Daily [n]:* `must be an integer greater than 0`' raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
fiat = self.freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
for day in range(0, timescale): for day in range(0, timescale):
profitday = today - timedelta(days=day) profitday = today - timedelta(days=day)
trades = Trade.query \ trades = Trade.query \
@ -135,7 +153,7 @@ class RPC(object):
'trades': len(trades) 'trades': len(trades)
} }
stats = [ return [
[ [
key, key,
'{value:.8f} {symbol}'.format( '{value:.8f} {symbol}'.format(
@ -157,13 +175,10 @@ class RPC(object):
] ]
for key, value in profit_days.items() for key, value in profit_days.items()
] ]
return False, stats
def rpc_trade_statistics( def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]: self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
""" """ Returns cumulative profit statistics """
:return: cumulative profit statistics.
"""
trades = Trade.query.order_by(Trade.id).all() trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = [] profit_all_coin = []
@ -201,13 +216,13 @@ class RPC(object):
.order_by(sql.text('profit_sum DESC')).first() .order_by(sql.text('profit_sum DESC')).first()
if not best_pair: if not best_pair:
return True, '*Status:* `no closed trade`' raise RPCException('*Status:* `no closed trade`')
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
# FIX: we want to keep fiatconverter in a state/environment, # FIX: we want to keep fiatconverter in a state/environment,
# doing this will utilize its caching functionallity, instead we reinitialize it here # doing this will utilize its caching functionallity, instead we reinitialize it here
fiat = self.freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
# Prepare data to display # Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8) profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2) profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
@ -224,35 +239,29 @@ class RPC(object):
fiat_display_currency fiat_display_currency
) )
num = float(len(durations) or 1) num = float(len(durations) or 1)
return ( return {
False, 'profit_closed_coin': profit_closed_coin,
{ 'profit_closed_percent': profit_closed_percent,
'profit_closed_coin': profit_closed_coin, 'profit_closed_fiat': profit_closed_fiat,
'profit_closed_percent': profit_closed_percent, 'profit_all_coin': profit_all_coin,
'profit_closed_fiat': profit_closed_fiat, 'profit_all_percent': profit_all_percent,
'profit_all_coin': profit_all_coin, 'profit_all_fiat': profit_all_fiat,
'profit_all_percent': profit_all_percent, 'trade_count': len(trades),
'profit_all_fiat': profit_all_fiat, 'first_trade_date': arrow.get(trades[0].open_date).humanize(),
'trade_count': len(trades), 'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'first_trade_date': arrow.get(trades[0].open_date).humanize(), 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(), 'best_pair': bp_pair,
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_rate': round(bp_rate * 100, 2),
'best_pair': bp_pair, }
'best_rate': round(bp_rate * 100, 2)
}
)
def rpc_balance(self, fiat_display_currency: str) -> Tuple[bool, Any]: def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
""" """ Returns current account balance per crypto """
:return: current account balance per crypto
"""
output = [] output = []
total = 0.0 total = 0.0
for coin, balance in exchange.get_balances().items(): for coin, balance in exchange.get_balances().items():
if not balance['total']: if not balance['total']:
continue continue
rate = None
if coin == 'BTC': if coin == 'BTC':
rate = 1.0 rate = 1.0
else: else:
@ -272,44 +281,39 @@ class RPC(object):
} }
) )
if total == 0.0: if total == 0.0:
return True, '`All balances are zero.`' raise RPCException('`All balances are zero.`')
fiat = self.freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
symbol = fiat_display_currency symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol) value = fiat.convert_amount(total, 'BTC', symbol)
return False, (output, total, symbol, value) return output, total, symbol, value
def rpc_start(self) -> Tuple[bool, str]: def _rpc_start(self) -> str:
""" """ Handler for start """
Handler for start. if self._freqtrade.state == State.RUNNING:
""" return '*Status:* `already running`'
if self.freqtrade.state == State.RUNNING:
return True, '*Status:* `already running`'
self.freqtrade.state = State.RUNNING self._freqtrade.state = State.RUNNING
return False, '`Starting trader ...`' return '`Starting trader ...`'
def rpc_stop(self) -> Tuple[bool, str]: def _rpc_stop(self) -> str:
""" """ Handler for stop """
Handler for stop. if self._freqtrade.state == State.RUNNING:
""" self._freqtrade.state = State.STOPPED
if self.freqtrade.state == State.RUNNING: return '`Stopping trader ...`'
self.freqtrade.state = State.STOPPED
return False, '`Stopping trader ...`'
return True, '*Status:* `already stopped`' return '*Status:* `already stopped`'
def rpc_reload_conf(self) -> str: def _rpc_reload_conf(self) -> str:
""" Handler for reload_conf. """ """ Handler for reload_conf. """
self.freqtrade.state = State.RELOAD_CONF self._freqtrade.state = State.RELOAD_CONF
return '*Status:* `Reloading config ...`' return '*Status:* `Reloading config ...`'
# FIX: no test for this!!!! # FIX: no test for this!!!!
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]: def _rpc_forcesell(self, trade_id) -> None:
""" """
Handler for forcesell <id>. Handler for forcesell <id>.
Sells the given trade at current price Sells the given trade at current price
:return: error or None
""" """
def _exec_forcesell(trade: Trade) -> None: def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order # Check if there is there is an open order
@ -335,17 +339,17 @@ class RPC(object):
# Get current rate and execute sell # Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid'] current_rate = exchange.get_ticker(trade.pair, False)['bid']
self.freqtrade.execute_sell(trade, current_rate) self._freqtrade.execute_sell(trade, current_rate)
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '`trader is not running`' raise RPCException('`trader is not running`')
if trade_id == 'all': if trade_id == 'all':
# Execute sell for all open orders # Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade) _exec_forcesell(trade)
return False, '' return
# Query for trade # Query for trade
trade = Trade.query.filter( trade = Trade.query.filter(
@ -356,19 +360,18 @@ class RPC(object):
).first() ).first()
if not trade: if not trade:
logger.warning('forcesell: Invalid argument received') logger.warning('forcesell: Invalid argument received')
return True, 'Invalid argument.' raise RPCException('Invalid argument.')
_exec_forcesell(trade) _exec_forcesell(trade)
Trade.session.flush() Trade.session.flush()
return False, ''
def rpc_performance(self) -> Tuple[bool, Any]: def _rpc_performance(self) -> List[Dict]:
""" """
Handler for performance. Handler for performance.
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
""" """
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '`trader is not running`' raise RPCException('`trader is not running`')
pair_rates = Trade.session.query(Trade.pair, pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'), sql.func.sum(Trade.close_profit).label('profit_sum'),
@ -377,19 +380,14 @@ class RPC(object):
.group_by(Trade.pair) \ .group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \ .order_by(sql.text('profit_sum DESC')) \
.all() .all()
trades = [] return [
for (pair, rate, count) in pair_rates: {'pair': pair, 'profit': round(rate * 100, 2), 'count': count}
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count}) for pair, rate, count in pair_rates
]
return False, trades def _rpc_count(self) -> List[Trade]:
""" Returns the number of trades running """
if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`')
def rpc_count(self) -> Tuple[bool, Any]: return Trade.query.filter(Trade.is_open.is_(True)).all()
"""
Returns the number of trades running
:return: None
"""
if self.freqtrade.state != State.RUNNING:
return True, '`trader is not running`'
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
return False, trades

View File

@ -1,11 +1,10 @@
""" """
This module contains class to manage RPC communications (Telegram, Slack, ...) This module contains class to manage RPC communications (Telegram, Slack, ...)
""" """
from typing import Any, List
import logging import logging
from typing import List
from freqtrade.rpc.telegram import Telegram from freqtrade.rpc.rpc import RPC
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,36 +14,23 @@ class RPCManager(object):
Class to manage RPC objects (Telegram, Slack, ...) Class to manage RPC objects (Telegram, Slack, ...)
""" """
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """ Initializes all enabled rpc modules """
Initializes all enabled rpc modules self.registered_modules: List[RPC] = []
:param config: config to use
:return: None
"""
self.freqtrade = freqtrade
self.registered_modules: List[str] = [] # Enable telegram
self.telegram: Any = None if freqtrade.config['telegram'].get('enabled', False):
self._init()
def _init(self) -> None:
"""
Init RPC modules
:return:
"""
if self.freqtrade.config['telegram'].get('enabled', False):
logger.info('Enabling rpc.telegram ...') logger.info('Enabling rpc.telegram ...')
self.registered_modules.append('telegram') from freqtrade.rpc.telegram import Telegram
self.telegram = Telegram(self.freqtrade) self.registered_modules.append(Telegram(freqtrade))
def cleanup(self) -> None: def cleanup(self) -> None:
""" """ Stops all enabled rpc modules """
Stops all enabled rpc modules logger.info('Cleaning up rpc modules ...')
:return: None while self.registered_modules:
""" mod = self.registered_modules.pop()
if 'telegram' in self.registered_modules: logger.debug('Cleaning up rpc.%s ...', mod.name)
logger.info('Cleaning up rpc.telegram ...') mod.cleanup()
self.registered_modules.remove('telegram') del mod
self.telegram.cleanup()
def send_msg(self, msg: str) -> None: def send_msg(self, msg: str) -> None:
""" """
@ -52,6 +38,7 @@ class RPCManager(object):
:param msg: message :param msg: message
:return: None :return: None
""" """
logger.info(msg) logger.info('Sending rpc message: %s', msg)
if 'telegram' in self.registered_modules: for mod in self.registered_modules:
self.telegram.send_msg(msg) logger.debug('Forwarding message to rpc.%s', mod.name)
mod.send_msg(msg)

View File

@ -12,11 +12,12 @@ from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC, RPCException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...')
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]: def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
""" """
@ -25,9 +26,7 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
:return: decorated function :return: decorated function
""" """
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
""" """ Decorator logic """
Decorator logic
"""
update = kwargs.get('update') or args[1] update = kwargs.get('update') or args[1]
# Reject unauthorized messages # Reject unauthorized messages
@ -54,9 +53,12 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
class Telegram(RPC): class Telegram(RPC):
""" """ This class handles all telegram communication """
Telegram, this class send messages to Telegram
""" @property
def name(self) -> str:
return "telegram"
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """
Init the Telegram call, and init the super class RPC Init the Telegram call, and init the super class RPC
@ -74,12 +76,7 @@ class Telegram(RPC):
Initializes this module with the given config, Initializes this module with the given config,
registers all known command handlers registers all known command handlers
and starts polling for message updates and starts polling for message updates
:param config: config to use
:return: None
""" """
if not self.is_enabled():
return
self._updater = Updater(token=self._config['telegram']['token'], workers=0) self._updater = Updater(token=self._config['telegram']['token'], workers=0)
# Register command handler and start telegram message polling # Register command handler and start telegram message polling
@ -115,16 +112,11 @@ class Telegram(RPC):
Stops all running telegram threads. Stops all running telegram threads.
:return: None :return: None
""" """
if not self.is_enabled():
return
self._updater.stop() self._updater.stop()
def is_enabled(self) -> bool: def send_msg(self, msg: str) -> None:
""" """ Send a message to telegram channel """
Returns True if the telegram module is activated, False otherwise self._send_msg(msg)
"""
return bool(self._config.get('telegram', {}).get('enabled', False))
@authorized_only @authorized_only
def _status(self, bot: Bot, update: Update) -> None: def _status(self, bot: Bot, update: Update) -> None:
@ -143,13 +135,11 @@ class Telegram(RPC):
self._status_table(bot, update) self._status_table(bot, update)
return return
# Fetch open trade try:
(error, trades) = self.rpc_trade_status() for trade_msg in self._rpc_trade_status():
if error: self._send_msg(trade_msg, bot=bot)
self.send_msg(trades, bot=bot) except RPCException as e:
else: self._send_msg(str(e), bot=bot)
for trademsg in trades:
self.send_msg(trademsg, bot=bot)
@authorized_only @authorized_only
def _status_table(self, bot: Bot, update: Update) -> None: def _status_table(self, bot: Bot, update: Update) -> None:
@ -160,15 +150,12 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
# Fetch open trade try:
(err, df_statuses) = self.rpc_status_table() df_statuses = self._rpc_status_table()
if err:
self.send_msg(df_statuses, bot=bot)
else:
message = tabulate(df_statuses, headers='keys', tablefmt='simple') message = tabulate(df_statuses, headers='keys', tablefmt='simple')
message = "<pre>{}</pre>".format(message) self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML)
except RPCException as e:
self.send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _daily(self, bot: Bot, update: Update) -> None: def _daily(self, bot: Bot, update: Update) -> None:
@ -183,14 +170,12 @@ class Telegram(RPC):
timescale = int(update.message.text.replace('/daily', '').strip()) timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError): except (TypeError, ValueError):
timescale = 7 timescale = 7
(error, stats) = self.rpc_daily_profit( try:
timescale, stats = self._rpc_daily_profit(
self._config['stake_currency'], timescale,
self._config['fiat_display_currency'] self._config['stake_currency'],
) self._config['fiat_display_currency']
if error: )
self.send_msg(stats, bot=bot)
else:
stats = tabulate(stats, stats = tabulate(stats,
headers=[ headers=[
'Day', 'Day',
@ -199,11 +184,10 @@ class Telegram(RPC):
], ],
tablefmt='simple') tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\ message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
.format( .format(timescale, stats)
timescale, self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
stats except RPCException as e:
) self._send_msg(str(e), bot=bot)
self.send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _profit(self, bot: Bot, update: Update) -> None: def _profit(self, bot: Bot, update: Update) -> None:
@ -214,67 +198,63 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, stats) = self.rpc_trade_statistics( try:
self._config['stake_currency'], stats = self._rpc_trade_statistics(
self._config['fiat_display_currency'] self._config['stake_currency'],
) self._config['fiat_display_currency'])
if error:
self.send_msg(stats, bot=bot)
return
# Message to display # Message to display
markdown_msg = "*ROI:* Close trades\n" \ markdown_msg = "*ROI:* Close trades\n" \
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \ "∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \ "∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
"*ROI:* All trades\n" \ "*ROI:* All trades\n" \
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \ "∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \ "∙ `{profit_all_fiat:.3f} {fiat}`\n" \
"*Total Trade Count:* `{trade_count}`\n" \ "*Total Trade Count:* `{trade_count}`\n" \
"*First Trade opened:* `{first_trade_date}`\n" \ "*First Trade opened:* `{first_trade_date}`\n" \
"*Latest Trade opened:* `{latest_trade_date}`\n" \ "*Latest Trade opened:* `{latest_trade_date}`\n" \
"*Avg. Duration:* `{avg_duration}`\n" \ "*Avg. Duration:* `{avg_duration}`\n" \
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\ "*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
.format( .format(
coin=self._config['stake_currency'], coin=self._config['stake_currency'],
fiat=self._config['fiat_display_currency'], fiat=self._config['fiat_display_currency'],
profit_closed_coin=stats['profit_closed_coin'], profit_closed_coin=stats['profit_closed_coin'],
profit_closed_percent=stats['profit_closed_percent'], profit_closed_percent=stats['profit_closed_percent'],
profit_closed_fiat=stats['profit_closed_fiat'], profit_closed_fiat=stats['profit_closed_fiat'],
profit_all_coin=stats['profit_all_coin'], profit_all_coin=stats['profit_all_coin'],
profit_all_percent=stats['profit_all_percent'], profit_all_percent=stats['profit_all_percent'],
profit_all_fiat=stats['profit_all_fiat'], profit_all_fiat=stats['profit_all_fiat'],
trade_count=stats['trade_count'], trade_count=stats['trade_count'],
first_trade_date=stats['first_trade_date'], first_trade_date=stats['first_trade_date'],
latest_trade_date=stats['latest_trade_date'], latest_trade_date=stats['latest_trade_date'],
avg_duration=stats['avg_duration'], avg_duration=stats['avg_duration'],
best_pair=stats['best_pair'], best_pair=stats['best_pair'],
best_rate=stats['best_rate'] best_rate=stats['best_rate']
) )
self.send_msg(markdown_msg, bot=bot) self._send_msg(markdown_msg, bot=bot)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _balance(self, bot: Bot, update: Update) -> None: def _balance(self, bot: Bot, update: Update) -> None:
""" """ Handler for /balance """
Handler for /balance try:
""" currencys, total, symbol, value = \
(error, result) = self.rpc_balance(self._config['fiat_display_currency']) self._rpc_balance(self._config['fiat_display_currency'])
if error: output = ''
self.send_msg('`All balances are zero.`') for currency in currencys:
return output += "*{currency}:*\n" \
"\t`Available: {available: .8f}`\n" \
"\t`Balance: {balance: .8f}`\n" \
"\t`Pending: {pending: .8f}`\n" \
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
(currencys, total, symbol, value) = result output += "\n*Estimated Value*:\n" \
output = '' "\t`BTC: {0: .8f}`\n" \
for currency in currencys: "\t`{1}: {2: .2f}`\n".format(total, symbol, value)
output += "*{currency}:*\n" \ self._send_msg(output, bot=bot)
"\t`Available: {available: .8f}`\n" \ except RPCException as e:
"\t`Balance: {balance: .8f}`\n" \ self._send_msg(str(e), bot=bot)
"\t`Pending: {pending: .8f}`\n" \
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
output += "\n*Estimated Value*:\n" \
"\t`BTC: {0: .8f}`\n" \
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
self.send_msg(output)
@authorized_only @authorized_only
def _start(self, bot: Bot, update: Update) -> None: def _start(self, bot: Bot, update: Update) -> None:
@ -285,9 +265,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, msg) = self.rpc_start() msg = self._rpc_start()
if error: self._send_msg(msg, bot=bot)
self.send_msg(msg, bot=bot)
@authorized_only @authorized_only
def _stop(self, bot: Bot, update: Update) -> None: def _stop(self, bot: Bot, update: Update) -> None:
@ -298,8 +277,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, msg) = self.rpc_stop() msg = self._rpc_stop()
self.send_msg(msg, bot=bot) self._send_msg(msg, bot=bot)
@authorized_only @authorized_only
def _reload_conf(self, bot: Bot, update: Update) -> None: def _reload_conf(self, bot: Bot, update: Update) -> None:
@ -310,8 +289,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
msg = self.rpc_reload_conf() msg = self._rpc_reload_conf()
self.send_msg(msg, bot=bot) self._send_msg(msg, bot=bot)
@authorized_only @authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None: def _forcesell(self, bot: Bot, update: Update) -> None:
@ -324,10 +303,10 @@ class Telegram(RPC):
""" """
trade_id = update.message.text.replace('/forcesell', '').strip() trade_id = update.message.text.replace('/forcesell', '').strip()
(error, message) = self.rpc_forcesell(trade_id) try:
if error: self._rpc_forcesell(trade_id)
self.send_msg(message, bot=bot) except RPCException as e:
return self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _performance(self, bot: Bot, update: Update) -> None: def _performance(self, bot: Bot, update: Update) -> None:
@ -338,19 +317,18 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, trades) = self.rpc_performance() try:
if error: trades = self._rpc_performance()
self.send_msg(trades, bot=bot) stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
return index=i + 1,
pair=trade['pair'],
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format( profit=trade['profit'],
index=i + 1, count=trade['count']
pair=trade['pair'], ) for i, trade in enumerate(trades))
profit=trade['profit'], message = '<b>Performance:</b>\n{}'.format(stats)
count=trade['count'] self._send_msg(message, parse_mode=ParseMode.HTML)
) for i, trade in enumerate(trades)) except RPCException as e:
message = '<b>Performance:</b>\n{}'.format(stats) self._send_msg(str(e), bot=bot)
self.send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _count(self, bot: Bot, update: Update) -> None: def _count(self, bot: Bot, update: Update) -> None:
@ -361,19 +339,18 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, trades) = self.rpc_count() try:
if error: trades = self._rpc_count()
self.send_msg(trades, bot=bot) message = tabulate({
return 'current': [len(trades)],
'max': [self._config['max_open_trades']],
message = tabulate({ 'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
'current': [len(trades)], }, headers=['current', 'max', 'total stake'], tablefmt='simple')
'max': [self._config['max_open_trades']], message = "<pre>{}</pre>".format(message)
'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)] logger.debug(message)
}, headers=['current', 'max', 'total stake'], tablefmt='simple') self._send_msg(message, parse_mode=ParseMode.HTML)
message = "<pre>{}</pre>".format(message) except RPCException as e:
logger.debug(message) self._send_msg(str(e), bot=bot)
self.send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _help(self, bot: Bot, update: Update) -> None: def _help(self, bot: Bot, update: Update) -> None:
@ -399,7 +376,7 @@ class Telegram(RPC):
"*/help:* `This help message`\n" \ "*/help:* `This help message`\n" \
"*/version:* `Show version`" "*/version:* `Show version`"
self.send_msg(message, bot=bot) self._send_msg(message, bot=bot)
@authorized_only @authorized_only
def _version(self, bot: Bot, update: Update) -> None: def _version(self, bot: Bot, update: Update) -> None:
@ -410,10 +387,10 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
self.send_msg('*Version:* `{}`'.format(__version__), bot=bot) self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def send_msg(self, msg: str, bot: Bot = None, def _send_msg(self, msg: str, bot: Bot = None,
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message
:param msg: message :param msg: message
@ -421,9 +398,6 @@ class Telegram(RPC):
:param parse_mode: telegram parse mode :param parse_mode: telegram parse mode
:return: None :return: None
""" """
if not self.is_enabled():
return
bot = bot or self._updater.bot bot = bot or self._updater.bot
keyboard = [['/daily', '/profit', '/balance'], keyboard = [['/daily', '/profit', '/balance'],

View File

@ -376,10 +376,10 @@ def test_generate_text_table(default_conf, mocker):
results = pd.DataFrame( results = pd.DataFrame(
{ {
'currency': ['ETH/BTC', 'ETH/BTC'], 'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2], 'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4], 'profit_abs': [0.2, 0.4],
'duration': [10, 30], 'trade_duration': [10, 30],
'profit': [2, 0], 'profit': [2, 0],
'loss': [0, 0] 'loss': [0, 0]
} }
@ -439,6 +439,40 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
assert log_has(line, caplog.record_tuples) assert log_has(line, caplog.record_tuples)
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)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.get_ticker_history')
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
_generate_text_table=MagicMock(return_value='1'),
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'
backtesting = Backtesting(conf)
backtesting.start()
# check the logs, that will contain the backtest result
assert log_has('No data found. Terminating.', caplog.record_tuples)
def test_backtest(default_conf, fee, mocker) -> None: def test_backtest(default_conf, fee, mocker) -> None:
""" """
Test Backtesting.backtest() method Test Backtesting.backtest() method
@ -458,6 +492,7 @@ def test_backtest(default_conf, fee, mocker) -> None:
} }
) )
assert not results.empty assert not results.empty
assert len(results) == 2
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
@ -480,6 +515,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
} }
) )
assert not results.empty assert not results.empty
assert len(results) == 1
def test_processed(default_conf, mocker) -> None: def test_processed(default_conf, mocker) -> None:
@ -501,7 +537,7 @@ def test_processed(default_conf, mocker) -> None:
def test_backtest_pricecontours(default_conf, fee, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee) mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
tests = [['raise', 17], ['lower', 0], ['sine', 16]] tests = [['raise', 18], ['lower', 0], ['sine', 16]]
for [contour, numres] in tests: for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres, mocker) simple_backtest(default_conf, contour, numres, mocker)
@ -561,7 +597,10 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
backtesting.populate_buy_trend = _trend_alternate # Override backtesting.populate_buy_trend = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override backtesting.populate_sell_trend = _trend_alternate # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert len(results) == 3 backtesting._store_backtest_result("test_.json", results)
assert len(results) == 4
# One trade was force-closed at the end
assert len(results.loc[results.open_at_end]) == 1
def test_backtest_record(default_conf, fee, mocker): def test_backtest_record(default_conf, fee, mocker):
@ -573,22 +612,30 @@ def test_backtest_record(default_conf, fee, mocker):
'freqtrade.optimize.backtesting.file_dump_json', 'freqtrade.optimize.backtesting.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r)) new=lambda n, r: (names.append(n), records.append(r))
) )
backtest_conf = _make_backtest_conf(
mocker,
conf=default_conf,
pair='UNITTEST/BTC',
record="trades"
)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
backtesting.populate_sell_trend = _trend_alternate # Override "UNITTEST/BTC", "UNITTEST/BTC"],
results = backtesting.backtest(backtest_conf) "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
assert len(results) == 3 "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
Arrow(2017, 11, 14, 22, 12, 00).datetime,
Arrow(2017, 11, 14, 22, 44, 00).datetime],
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_index": [1, 119, 153, 185],
"close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14]})
backtesting._store_backtest_result("backtest-result.json", results)
assert len(results) == 4
# Assert file_dump_json was only called once # Assert file_dump_json was only called once
assert names == ['backtest-result.json'] assert names == ['backtest-result.json']
records = records[0] records = records[0]
# Ensure records are of correct type # Ensure records are of correct type
assert len(records) == 3 assert len(records) == 4
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records # Below follows just a typecheck of the schema/type of trade-records
oix = None oix = None

View File

@ -23,8 +23,6 @@ def init_hyperopt(default_conf, mocker):
global _HYPEROPT_INITIALIZED, _HYPEROPT global _HYPEROPT_INITIALIZED, _HYPEROPT
if not _HYPEROPT_INITIALIZED: if not _HYPEROPT_INITIALIZED:
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf',
MagicMock(return_value=default_conf))
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
_HYPEROPT = Hyperopt(default_conf) _HYPEROPT = Hyperopt(default_conf)
_HYPEROPT_INITIALIZED = True _HYPEROPT_INITIALIZED = True
@ -64,8 +62,6 @@ def test_start(mocker, default_conf, caplog) -> None:
""" """
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf',
MagicMock(return_value=default_conf))
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
args = [ args = [
@ -182,7 +178,6 @@ def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result) mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
StrategyResolver({'strategy': 'DefaultStrategy'}) StrategyResolver({'strategy': 'DefaultStrategy'})
@ -227,7 +222,6 @@ def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) ->
conf.update({'epochs': 1}) conf.update({'epochs': 1})
conf.update({'timerange': None}) conf.update({'timerange': None})
conf.update({'spaces': 'all'}) conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
StrategyResolver({'strategy': 'DefaultStrategy'}) StrategyResolver({'strategy': 'DefaultStrategy'})
@ -253,7 +247,6 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, defa
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'}) conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1}) conf.update({'epochs': 1})
conf.update({'mongodb': False})
conf.update({'timerange': None}) conf.update({'timerange': None})
conf.update({'spaces': 'all'}) conf.update({'spaces': 'all'})
@ -270,7 +263,6 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, defa
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results) mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
StrategyResolver({'strategy': 'DefaultStrategy'}) StrategyResolver({'strategy': 'DefaultStrategy'})
@ -348,7 +340,6 @@ def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'}) conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1}) conf.update({'epochs': 1})
conf.update({'mongodb': False})
conf.update({'timerange': None}) conf.update({'timerange': None})
conf.update({'spaces': 'all'}) conf.update({'spaces': 'all'})
@ -360,35 +351,6 @@ def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
mock_fmin.assert_called_once() mock_fmin.assert_called_once()
def test_start_uses_mongotrials(mocker, init_hyperopt, default_conf) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
mock_mongotrials = mocker.patch(
'freqtrade.optimize.hyperopt.MongoTrials',
return_value=create_trials(mocker)
)
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'mongodb': True})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
hyperopt = Hyperopt(conf)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_mongotrials.assert_called_once()
mock_fmin.assert_called_once()
# test log_trials_result
# test buy_strategy_generator def populate_buy_trend
# test optimizer if 'ro_t1' in params
def test_format_results(init_hyperopt): def test_format_results(init_hyperopt):
""" """
Test Hyperopt.format_results() Test Hyperopt.format_results()
@ -400,7 +362,7 @@ def test_format_results(init_hyperopt):
('LTC/BTC', 1, 1, 123), ('LTC/BTC', 1, 1, 123),
('XPR/BTC', -1, -2, -246) ('XPR/BTC', -1, -2, -246)
] ]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
df = pd.DataFrame.from_records(trades, columns=labels) df = pd.DataFrame.from_records(trades, columns=labels)
result = _HYPEROPT.format_results(df) result = _HYPEROPT.format_results(df)
@ -530,7 +492,7 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
trades = [ trades = [
('POWR/BTC', 0.023117, 0.000233, 100) ('POWR/BTC', 0.023117, 0.000233, 100)
] ]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
backtest_result = pd.DataFrame.from_records(trades, columns=labels) backtest_result = pd.DataFrame.from_records(trades, columns=labels)
mocker.patch( mocker.patch(

View File

@ -1,16 +0,0 @@
# pragma pylint: disable=missing-docstring,W0212
from user_data.hyperopt_conf import hyperopt_optimize_conf
def test_hyperopt_optimize_conf():
hyperopt_conf = hyperopt_optimize_conf()
assert "max_open_trades" in hyperopt_conf
assert "stake_currency" in hyperopt_conf
assert "stake_amount" in hyperopt_conf
assert "minimal_roi" in hyperopt_conf
assert "stoploss" in hyperopt_conf
assert "bid_strategy" in hyperopt_conf
assert "exchange" in hyperopt_conf
assert "pair_whitelist" in hyperopt_conf['exchange']

View File

@ -326,8 +326,6 @@ def test_load_tickerdata_file() -> None:
def test_init(default_conf, mocker) -> None: def test_init(default_conf, mocker) -> None:
conf = {'exchange': {'pair_whitelist': []}}
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
assert {} == optimize.load_data( assert {} == optimize.load_data(
'', '',
pairs=[], pairs=[],

View File

@ -7,9 +7,11 @@ Unit test file for rpc/rpc.py
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC, RPCException
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
@ -29,7 +31,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -42,19 +44,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_trade_status() with pytest.raises(RPCException, match=r'.*trader is not running*'):
assert error rpc._rpc_trade_status()
assert 'trader is not running' in result
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_trade_status() with pytest.raises(RPCException, match=r'.*no active trade*'):
assert error rpc._rpc_trade_status()
assert 'no active trade' in result
freqtradebot.create_trade() freqtradebot.create_trade()
(error, result) = rpc.rpc_trade_status() trades = rpc._rpc_trade_status()
assert not error trade = trades[0]
trade = result[0]
result_message = [ result_message = [
'*Trade ID:* `1`\n' '*Trade ID:* `1`\n'
@ -69,7 +68,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
'*Current Profit:* `-0.59%`\n' '*Current Profit:* `-0.59%`\n'
'*Open Order:* `(limit buy rem=0.00000000)`' '*Open Order:* `(limit buy rem=0.00000000)`'
] ]
assert result == result_message assert trades == result_message
assert trade.find('[ETH/BTC]') >= 0 assert trade.find('[ETH/BTC]') >= 0
@ -79,7 +78,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -92,17 +91,15 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_status_table() with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'):
assert error rpc._rpc_status_table()
assert '*Status:* `trader is not running`' in result
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_status_table() with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'):
assert error rpc._rpc_status_table()
assert '*Status:* `no active order`' in result
freqtradebot.create_trade() freqtradebot.create_trade()
(error, result) = rpc.rpc_status_table() result = rpc._rpc_status_table()
assert 'just now' in result['Since'].all() assert 'just now' in result['Since'].all()
assert 'ETH/BTC' in result['Pair'].all() assert 'ETH/BTC' in result['Pair'].all()
assert '-0.59%' in result['Profit'].all() assert '-0.59%' in result['Profit'].all()
@ -115,7 +112,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -143,8 +140,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
# Try valid data # Try valid data
update.message.text = '/daily 2' update.message.text = '/daily 2'
(error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency) days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert not error
assert len(days) == 7 assert len(days) == 7
for day in days: for day in days:
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD'] # [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
@ -157,9 +153,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
assert str(days[0][0]) == str(datetime.utcnow().date()) assert str(days[0][0]) == str(datetime.utcnow().date())
# Try invalid data # Try invalid data
(error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency) with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
assert error rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
assert days.find('must be an integer greater than 0') >= 0
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
@ -173,7 +168,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -188,9 +183,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) with pytest.raises(RPCException, match=r'.*no closed trade*'):
assert error rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats.find('no closed trade') >= 0
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trade()
@ -223,8 +217,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert not error
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2) assert prec_satoshi(stats['profit_closed_percent'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255) assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
@ -252,7 +245,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -286,8 +279,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
for trade in Trade.query.order_by(Trade.id).all(): for trade in Trade.query.order_by(Trade.id).all():
trade.open_rate = None trade.open_rate = None
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert not error
assert prec_satoshi(stats['profit_closed_coin'], 0) assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent'], 0) assert prec_satoshi(stats['profit_closed_percent'], 0)
assert prec_satoshi(stats['profit_closed_fiat'], 0) assert prec_satoshi(stats['profit_closed_fiat'], 0)
@ -325,7 +317,7 @@ def test_rpc_balance_handle(default_conf, mocker):
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -335,18 +327,16 @@ def test_rpc_balance_handle(default_conf, mocker):
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency']) output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency'])
assert not error assert prec_satoshi(total, 12)
(trade, x, y, z) = res assert prec_satoshi(value, 180000)
assert prec_satoshi(x, 12) assert 'USD' in symbol
assert prec_satoshi(z, 180000) assert len(output) == 1
assert 'USD' in y assert 'BTC' in output[0]['currency']
assert len(trade) == 1 assert prec_satoshi(output[0]['available'], 10)
assert 'BTC' in trade[0]['currency'] assert prec_satoshi(output[0]['balance'], 12)
assert prec_satoshi(trade[0]['available'], 10) assert prec_satoshi(output[0]['pending'], 2)
assert prec_satoshi(trade[0]['balance'], 12) assert prec_satoshi(output[0]['est_btc'], 12)
assert prec_satoshi(trade[0]['pending'], 2)
assert prec_satoshi(trade[0]['est_btc'], 12)
def test_rpc_start(mocker, default_conf) -> None: def test_rpc_start(mocker, default_conf) -> None:
@ -355,7 +345,7 @@ def test_rpc_start(mocker, default_conf) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -366,13 +356,11 @@ def test_rpc_start(mocker, default_conf) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_start() result = rpc._rpc_start()
assert not error
assert '`Starting trader ...`' in result assert '`Starting trader ...`' in result
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
(error, result) = rpc.rpc_start() result = rpc._rpc_start()
assert error
assert '*Status:* `already running`' in result assert '*Status:* `already running`' in result
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
@ -383,7 +371,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -394,13 +382,11 @@ def test_rpc_stop(mocker, default_conf) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_stop() result = rpc._rpc_stop()
assert not error
assert '`Stopping trader ...`' in result assert '`Stopping trader ...`' in result
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
(error, result) = rpc.rpc_stop() result = rpc._rpc_stop()
assert error
assert '*Status:* `already stopped`' in result assert '*Status:* `already stopped`' in result
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
@ -411,7 +397,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@ -434,36 +420,26 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None) with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
assert error rpc._rpc_forcesell(None)
assert res == '`trader is not running`'
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, res) = rpc.rpc_forcesell(None) with pytest.raises(RPCException, match=r'.*Invalid argument.*'):
assert error rpc._rpc_forcesell(None)
assert res == 'Invalid argument.'
(error, res) = rpc.rpc_forcesell('all') rpc._rpc_forcesell('all')
assert not error
assert res == ''
freqtradebot.create_trade() freqtradebot.create_trade()
(error, res) = rpc.rpc_forcesell('all') rpc._rpc_forcesell('all')
assert not error
assert res == ''
(error, res) = rpc.rpc_forcesell('1') rpc._rpc_forcesell('1')
assert not error
assert res == ''
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None) with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
assert error rpc._rpc_forcesell(None)
assert res == '`trader is not running`'
(error, res) = rpc.rpc_forcesell('all') with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
assert error rpc._rpc_forcesell('all')
assert res == '`trader is not running`'
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
@ -481,9 +457,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
) )
# check that the trade is called, which is done by ensuring exchange.cancel_order is called # check that the trade is called, which is done by ensuring exchange.cancel_order is called
# and trade amount is updated # and trade amount is updated
(error, res) = rpc.rpc_forcesell('1') rpc._rpc_forcesell('1')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount assert trade.amount == filled_amount
@ -501,9 +475,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
} }
) )
# check that the trade is called, which is done by ensuring exchange.cancel_order is called # check that the trade is called, which is done by ensuring exchange.cancel_order is called
(error, res) = rpc.rpc_forcesell('2') rpc._rpc_forcesell('2')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 2 assert cancel_order_mock.call_count == 2
assert trade.amount == amount assert trade.amount == amount
@ -517,9 +489,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
'side': 'sell' 'side': 'sell'
} }
) )
(error, res) = rpc.rpc_forcesell('3') rpc._rpc_forcesell('3')
assert not error
assert res == ''
# status quo, no exchange calls # status quo, no exchange calls
assert cancel_order_mock.call_count == 2 assert cancel_order_mock.call_count == 2
@ -531,7 +501,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -557,8 +527,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
(error, res) = rpc.rpc_performance() res = rpc._rpc_performance()
assert not error
assert len(res) == 1 assert len(res) == 1
assert res[0]['pair'] == 'ETH/BTC' assert res[0]['pair'] == 'ETH/BTC'
assert res[0]['count'] == 1 assert res[0]['count'] == 1
@ -571,7 +540,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -584,14 +553,12 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
(error, trades) = rpc.rpc_count() trades = rpc._rpc_count()
nb_trades = len(trades) nb_trades = len(trades)
assert not error
assert nb_trades == 0 assert nb_trades == 0
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trade()
(error, trades) = rpc.rpc_count() trades = rpc._rpc_count()
nb_trades = len(trades) nb_trades = len(trades)
assert not error
assert nb_trades == 1 assert nb_trades == 1

View File

@ -7,49 +7,35 @@ from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.rpc.rpc_manager import RPCManager from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.rpc.telegram import Telegram
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
def test_rpc_manager_object() -> None: def test_rpc_manager_object() -> None:
""" """ Test the Arguments object has the mandatory methods """
Test the Arguments object has the mandatory methods
:return: None
"""
assert hasattr(RPCManager, '_init')
assert hasattr(RPCManager, 'send_msg') assert hasattr(RPCManager, 'send_msg')
assert hasattr(RPCManager, 'cleanup') assert hasattr(RPCManager, 'cleanup')
def test__init__(mocker, default_conf) -> None: def test__init__(mocker, default_conf) -> None:
""" """ Test __init__() method """
Test __init__() method conf = deepcopy(default_conf)
""" conf['telegram']['enabled'] = False
init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert rpc_manager.freqtrade == freqtradebot
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
assert init_mock.call_count == 1
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
""" """ Test _init() method with Telegram disabled """
Test _init() method with Telegram disabled
"""
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
rpc_manager = RPCManager(freqtradebot)
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
@ -59,14 +45,12 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
rpc_manager = RPCManager(freqtradebot)
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
len_modules = len(rpc_manager.registered_modules) len_modules = len(rpc_manager.registered_modules)
assert len_modules == 1 assert len_modules == 1
assert 'telegram' in rpc_manager.registered_modules assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
assert isinstance(rpc_manager.telegram, Telegram)
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
@ -99,11 +83,11 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
# Check we have Telegram as a registered modules # Check we have Telegram as a registered modules
assert 'telegram' in rpc_manager.registered_modules assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
rpc_manager.cleanup() rpc_manager.cleanup()
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples) assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
assert 'telegram' not in rpc_manager.registered_modules assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1
@ -120,7 +104,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test') rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples) assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 0 assert telegram_mock.call_count == 0
@ -135,5 +119,5 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test') rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples) assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1

View File

@ -32,6 +32,9 @@ class DummyCls(Telegram):
super().__init__(freqtrade) super().__init__(freqtrade)
self.state = {'called': False} self.state = {'called': False}
def _init(self):
pass
@authorized_only @authorized_only
def dummy_handler(self, *args, **kwargs) -> None: def dummy_handler(self, *args, **kwargs) -> None:
""" """
@ -60,9 +63,7 @@ def test__init__(default_conf, mocker) -> None:
def test_init(default_conf, mocker, caplog) -> None: def test_init(default_conf, mocker, caplog) -> None:
""" """ Test _init() method """
Test _init() method
"""
start_polling = MagicMock() start_polling = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
@ -80,21 +81,6 @@ def test_init(default_conf, mocker, caplog) -> None:
assert log_has(message_str, caplog.record_tuples) assert log_has(message_str, caplog.record_tuples)
def test_init_disabled(default_conf, mocker, caplog) -> None:
"""
Test _init() method when Telegram is disabled
"""
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
Telegram(get_patched_freqtradebot(mocker, conf))
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
"['count'], ['help'], ['version']]"
assert not log_has(message_str, caplog.record_tuples)
def test_cleanup(default_conf, mocker) -> None: def test_cleanup(default_conf, mocker) -> None:
""" """
Test cleanup() method Test cleanup() method
@ -103,44 +89,11 @@ def test_cleanup(default_conf, mocker) -> None:
updater_mock.stop = MagicMock() updater_mock.stop = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
# not enabled telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
telegram.cleanup()
assert telegram._updater is None
assert updater_mock.call_count == 0
assert not hasattr(telegram._updater, 'stop')
assert updater_mock.stop.call_count == 0
# enabled
conf['telegram']['enabled'] = True
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
telegram.cleanup() telegram.cleanup()
assert telegram._updater.stop.call_count == 1 assert telegram._updater.stop.call_count == 1
def test_is_enabled(default_conf, mocker) -> None:
"""
Test is_enabled() method
"""
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
assert telegram.is_enabled()
def test_is_not_enabled(default_conf, mocker) -> None:
"""
Test is_enabled() method
"""
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
assert not telegram.is_enabled()
def test_authorized_only(default_conf, mocker, caplog) -> None: def test_authorized_only(default_conf, mocker, caplog) -> None:
""" """
Test authorized_only() method when we are authorized Test authorized_only() method when we are authorized
@ -257,9 +210,9 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
rpc_trade_status=MagicMock(return_value=(False, [1, 2, 3])), _rpc_trade_status=MagicMock(return_value=[1, 2, 3]),
_status_table=status_table, _status_table=status_table,
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -298,7 +251,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
_status_table=status_table, _status_table=status_table,
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -344,7 +297,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -401,7 +354,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -469,7 +422,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -511,7 +464,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -609,7 +562,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -639,7 +592,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -661,7 +614,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -672,7 +625,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
telegram._start(bot=MagicMock(), update=update) telegram._start(bot=MagicMock(), update=update)
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 0 assert msg_mock.call_count == 1
def test_start_handle_already_running(default_conf, update, mocker) -> None: def test_start_handle_already_running(default_conf, update, mocker) -> None:
@ -685,7 +638,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -710,7 +663,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -735,7 +688,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -758,7 +711,7 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -908,7 +861,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
@ -950,7 +903,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
@ -992,7 +945,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -1015,7 +968,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
@ -1059,7 +1012,7 @@ def test_help_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
@ -1079,7 +1032,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
@ -1101,13 +1054,8 @@ def test_send_msg(default_conf, mocker) -> None:
freqtradebot = FreqtradeBot(conf) freqtradebot = FreqtradeBot(conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = False
telegram.send_msg('test', bot)
assert not bot.method_calls
bot.reset_mock()
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
telegram.send_msg('test', bot) telegram._send_msg('test', bot)
assert len(bot.method_calls) == 1 assert len(bot.method_calls) == 1
@ -1125,7 +1073,7 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
telegram.send_msg('test', bot) telegram._send_msg('test', bot)
# Bot should've tried to send it twice # Bot should've tried to send it twice
assert len(bot.method_calls) == 2 assert len(bot.method_calls) == 2

View File

@ -322,7 +322,6 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
arglist = [ arglist = [
'hyperopt', 'hyperopt',
'--epochs', '10', '--epochs', '10',
'--use-mongodb',
'--spaces', 'all', '--spaces', 'all',
] ]
@ -336,10 +335,6 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
assert log_has('Parameter --epochs detected ...', caplog.record_tuples) assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples) assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
assert 'mongodb' in config
assert config['mongodb'] is True
assert log_has('Parameter --use-mongodb detected ...', caplog.record_tuples)
assert 'spaces' in config assert 'spaces' in config
assert config['spaces'] == ['all'] assert config['spaces'] == ['all']
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples) assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)

View File

@ -57,7 +57,7 @@ def patch_RPCManager(mocker) -> MagicMock:
:param mocker: mocker to patch RPCManager class :param mocker: mocker to patch RPCManager class
:return: RPCManager.send_msg MagicMock to track if this method is called :return: RPCManager.send_msg MagicMock to track if this method is called
""" """
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
rpc_mock = mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) rpc_mock = mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
return rpc_mock return rpc_mock

View File

@ -110,10 +110,13 @@ def heikinashi(bars):
bars = bars.copy() bars = bars.copy()
bars['ha_close'] = (bars['open'] + bars['high'] + bars['ha_close'] = (bars['open'] + bars['high'] +
bars['low'] + bars['close']) / 4 bars['low'] + bars['close']) / 4
bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2 bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
bars.loc[:1, 'ha_open'] = bars['open'].values[0] bars.loc[:1, 'ha_open'] = bars['open'].values[0]
bars.loc[1:, 'ha_open'] = ( for x in range(2):
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:] bars.loc[1:, 'ha_open'] = (
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1) bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1) bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
@ -248,45 +251,36 @@ def crossed_below(series1, series2):
def rolling_std(series, window=200, min_periods=None): def rolling_std(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods min_periods = window if min_periods is None else min_periods
try: if min_periods == window and len(series) > window:
if min_periods == window: return numpy_rolling_std(series, window, True)
return numpy_rolling_std(series, window, True) else:
else: try:
try: return series.rolling(window=window, min_periods=min_periods).std()
return series.rolling(window=window, min_periods=min_periods).std() except BaseException:
except BaseException: return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
except BaseException:
return pd.rolling_std(series, window=window, min_periods=min_periods)
# --------------------------------------------- # ---------------------------------------------
def rolling_mean(series, window=200, min_periods=None): def rolling_mean(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods min_periods = window if min_periods is None else min_periods
try: if min_periods == window and len(series) > window:
if min_periods == window: return numpy_rolling_mean(series, window, True)
return numpy_rolling_mean(series, window, True) else:
else: try:
try: return series.rolling(window=window, min_periods=min_periods).mean()
return series.rolling(window=window, min_periods=min_periods).mean() except BaseException:
except BaseException: return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
except BaseException:
return pd.rolling_mean(series, window=window, min_periods=min_periods)
# --------------------------------------------- # ---------------------------------------------
def rolling_min(series, window=14, min_periods=None): def rolling_min(series, window=14, min_periods=None):
min_periods = window if min_periods is None else min_periods min_periods = window if min_periods is None else min_periods
try: try:
try: return series.rolling(window=window, min_periods=min_periods).min()
return series.rolling(window=window, min_periods=min_periods).min()
except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except BaseException: except BaseException:
return pd.rolling_min(series, window=window, min_periods=min_periods) return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
# --------------------------------------------- # ---------------------------------------------
@ -294,12 +288,9 @@ def rolling_min(series, window=14, min_periods=None):
def rolling_max(series, window=14, min_periods=None): def rolling_max(series, window=14, min_periods=None):
min_periods = window if min_periods is None else min_periods min_periods = window if min_periods is None else min_periods
try: try:
try: return series.rolling(window=window, min_periods=min_periods).min()
return series.rolling(window=window, min_periods=min_periods).min()
except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except BaseException: except BaseException:
return pd.rolling_min(series, window=window, min_periods=min_periods) return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
# --------------------------------------------- # ---------------------------------------------
@ -566,9 +557,9 @@ def stoch(df, window=14, d=3, k=3, fast=False):
return pd.DataFrame(index=df.index, data=data) return pd.DataFrame(index=df.index, data=data)
# --------------------------------------------- # ---------------------------------------------
def zscore(bars, window=20, stds=1, col='close'): def zscore(bars, window=20, stds=1, col='close'):
""" get zscore of price """ """ get zscore of price """
std = numpy_rolling_std(bars[col], window) std = numpy_rolling_std(bars[col], window)

View File

@ -1,16 +1,16 @@
ccxt==1.14.172 ccxt==1.14.201
SQLAlchemy==1.2.8 SQLAlchemy==1.2.8
python-telegram-bot==10.1.0 python-telegram-bot==10.1.0
arrow==0.12.1 arrow==0.12.1
cachetools==2.1.0 cachetools==2.1.0
requests==2.18.4 requests==2.19.1
urllib3==1.22 urllib3==1.22
wrapt==1.10.11 wrapt==1.10.11
pandas==0.23.0 pandas==0.23.1
scikit-learn==0.19.1 scikit-learn==0.19.1
scipy==1.1.0 scipy==1.1.0
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.14.4 numpy==1.14.5
TA-Lib==0.4.17 TA-Lib==0.4.17
pytest==3.6.1 pytest==3.6.1
pytest-mock==1.10.0 pytest-mock==1.10.0

View File

@ -30,6 +30,8 @@ if not os.path.isfile(pairs_file):
with open(pairs_file) as file: with open(pairs_file) as file:
PAIRS = list(set(json.load(file))) PAIRS = list(set(json.load(file)))
PAIRS.sort()
since_time = None since_time = None
if args.days: if args.days:
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000 since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000
@ -41,9 +43,15 @@ print(f'About to download pairs: {PAIRS} to {dl_path}')
exchange._API = exchange.init_ccxt({'key': '', exchange._API = exchange.init_ccxt({'key': '',
'secret': '', 'secret': '',
'name': args.exchange}) 'name': args.exchange})
pairs_not_available = []
# Make sure API markets is initialized
exchange._API.load_markets()
for pair in PAIRS: for pair in PAIRS:
if pair not in exchange._API.markets:
pairs_not_available.append(pair)
print(f"skipping pair {pair}")
continue
for tick_interval in timeframes: for tick_interval in timeframes:
print(f'downloading pair {pair}, interval {tick_interval}') print(f'downloading pair {pair}, interval {tick_interval}')
@ -60,3 +68,7 @@ for pair in PAIRS:
pair_print = pair.replace('/', '_') pair_print = pair.replace('/', '_')
filename = f'{pair_print}-{tick_interval}.json' filename = f'{pair_print}-{tick_interval}.json'
misc.file_dump_json(os.path.join(dl_path, filename), data) misc.file_dump_json(os.path.join(dl_path, filename), data)
if pairs_not_available:
print(f"Pairs [{','.join(pairs_not_available)}] not availble.")

View File

@ -91,7 +91,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
tickers[pair] = exchange.get_ticker_history(pair, tick_interval) tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
else: else:
tickers = optimize.load_data( tickers = optimize.load_data(
datadir=args.datadir, datadir=_CONF.get("datadir"),
pairs=[pair], pairs=[pair],
ticker_interval=tick_interval, ticker_interval=tick_interval,
refresh_pairs=_CONF.get('refresh_pairs', False), refresh_pairs=_CONF.get('refresh_pairs', False),

View File

@ -121,7 +121,7 @@ def plot_profit(args: Namespace) -> None:
logger.info('Filter, keep pairs %s' % pairs) logger.info('Filter, keep pairs %s' % pairs)
tickers = optimize.load_data( tickers = optimize.load_data(
datadir=args.datadir, datadir=config.get('datadir'),
pairs=pairs, pairs=pairs,
ticker_interval=tick_interval, ticker_interval=tick_interval,
refresh_pairs=False, refresh_pairs=False,

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import multiprocessing
import os
import subprocess
PROC_COUNT = multiprocessing.cpu_count() - 1
DB_NAME = 'freqtrade_hyperopt'
WORK_DIR = os.path.join(
os.path.sep,
os.path.abspath(os.path.dirname(__file__)),
'..', '.hyperopt', 'worker'
)
if not os.path.exists(WORK_DIR):
os.makedirs(WORK_DIR)
# Spawn workers
command = [
'hyperopt-mongo-worker',
'--mongo=127.0.0.1:1234/{}'.format(DB_NAME),
'--poll-interval=0.1',
'--workdir={}'.format(WORK_DIR),
]
processes = [subprocess.Popen(command) for i in range(PROC_COUNT)]
# Join all workers
for proc in processes:
proc.wait()

View File

@ -1,21 +0,0 @@
#!/usr/bin/env python3
import os
import subprocess
DB_PATH = os.path.join(
os.path.sep,
os.path.abspath(os.path.dirname(__file__)),
'..', '.hyperopt', 'mongodb'
)
if not os.path.exists(DB_PATH):
os.makedirs(DB_PATH)
subprocess.Popen([
'mongod',
'--bind_ip=127.0.0.1',
'--port=1234',
'--nohttpinterface',
'--dbpath={}'.format(DB_PATH),
]).wait()

View File

@ -1,42 +0,0 @@
"""
File that contains the configuration for Hyperopt
"""
def hyperopt_optimize_conf() -> dict:
"""
This function is used to define which parameters Hyperopt must used.
The "pair_whitelist" is only used is your are using Hyperopt with MongoDB,
without MongoDB, Hyperopt will use the pair your have set in your config file.
:return:
"""
return {
'max_open_trades': 3,
'stake_currency': 'BTC',
'stake_amount': 0.01,
"minimal_roi": {
'40': 0.0,
'30': 0.01,
'20': 0.02,
'0': 0.04,
},
'stoploss': -0.10,
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"name": "bittrex",
"pair_whitelist": [
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"XLM/BTC",
"NXT/BTC",
"POWR/BTC",
"ADA/BTC",
"XMR/BTC"
]
}
}